@kangsicj/dcinsideenhancer 1.2.2 → 1.3.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
@@ -64,7 +64,8 @@ One userscript for DCInside on mobile and PC: smart blocking, endless lists, ima
64
64
  - Come back from a post and you're **right where you left off**
65
65
  - Cleaner read-post colors and a back-to-top button
66
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
67
+ - On PC, **press and hold** a title to preview the whole post, iPhone-style — blocked images and words stay hidden there too
68
+ - **Pin the order** of recently visited galleries — it stays put wherever you go
68
69
 
69
70
  </td>
70
71
  </tr>
package/README.md CHANGED
@@ -64,7 +64,8 @@
64
64
  - 글을 보고 돌아오면 **보던 위치 그대로**
65
65
  - 읽은 글 색 정리, 맨 위로 버튼
66
66
  - PC 키보드 단축키: **J/K** 다음·이전 글, Enter 열기, P 미리보기, S 스크랩, / 검색
67
- - PC는 제목에 마우스를 올리면 **글 전체 미리보기** — 차단 이미지·단어는 미리보기에서도 가림
67
+ - PC는 아이폰처럼 제목을 **길게 누르면 글 전체 미리보기** — 차단 이미지·단어는 미리보기에서도 가림
68
+ - 최근 방문 갤러리 **순서 고정** — 어디를 가도 정한 순서 그대로
68
69
 
69
70
  </td>
70
71
  </tr>
@@ -2,7 +2,7 @@
2
2
  // @name DCInside Enhancer
3
3
  // @name:ko 디시 인핸서
4
4
  // @namespace https://github.com/kangsicj/DCInsideEnhancer
5
- // @version 1.2.2
5
+ // @version 1.3.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.2.2
5
+ // @version 1.3.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.2.2';
78
+ const SCRIPT_VERSION = '1.3.0';
79
79
  const PROJECT_URL = 'https://github.com/kangsicj/DCInsideEnhancer';
80
80
  const DONATE_URL = 'https://fairy.hada.io/@jinny#support';
81
81
 
@@ -106,6 +106,7 @@
106
106
  syncLock: 'dc_sync_lock',
107
107
  syncRt: 'dc_sync_rt',
108
108
  recent: 'dc_recent_galls',
109
+ recentOrder: 'dc_recent_order',
109
110
  recentNative: 'dc_recent_native',
110
111
  searchHistory: 'dc_search_history',
111
112
  blockHits: 'dc_block_hits',
@@ -221,11 +222,13 @@
221
222
  hideAdSlots: true,
222
223
  hideNewsRows: true,
223
224
  hideCommentAds: true,
225
+ hideCommentFab: true,
224
226
  hideSilbeAlarm: true,
225
227
  alarmBubble: true,
226
228
  alarmKeepPolling: true,
227
229
  keyboardShortcuts: true,
228
230
  hoverPreview: true,
231
+ previewHover: false,
229
232
  alarmSystemNotify: false,
230
233
  pcFitWidth: true,
231
234
  autoHideScrollbar: true,
@@ -474,7 +477,17 @@
474
477
  html.dc-pc-fit .dcwrap :is(.cmt_info, .reply_info) > .fr { flex: 0 0 auto; float: none !important; margin-left: 10px; }
475
478
 
476
479
  html.dc-pc-fit .dcwrap .writing_view_box { float: none !important; width: auto !important; }
477
- html.dc-pc-fit .dcwrap .writing_view_box .write_div { width: auto !important; }
480
+ html.dc-pc-fit .dcwrap .writing_view_box .write_div {
481
+ width: auto !important;
482
+ max-width: min(100%, 900px) !important;
483
+ margin-left: auto !important;
484
+ margin-right: auto !important;
485
+ }
486
+ html.dc-pc-fit .dcwrap .writing_view_box .write_div :is(img, video, iframe) { margin-left: auto; margin-right: auto; }
487
+ html.dc-pc-fit .dcwrap .writing_view_box .write_div img { display: block; height: auto; }
488
+
489
+ html.dc-pc-fit .dcwrap .comment_count { display: flow-root; }
490
+ html.dc-pc-fit .dcwrap :is(.cmt_list, .comment_box) { clear: both; }
478
491
 
479
492
  html.dc-pc-fit .dcwrap .bottom_paging_wrap {
480
493
  display: flex !important;
@@ -507,6 +520,7 @@
507
520
  html.dc-ad-slots-off .dcwrap :is(.stickyunit, .rightbanner, .rightbanner1, .banner_box, .ad_bottom_list, #ad_floating, .kakao_ad_area, .ad_top, .view_ad_wrap, .power_link, .con_banner.writing_banbox, #ad_nv_slot, ins.adsbygoogle),
508
521
  html.dc-ad-slots-off :is(.adv-group, li.adv-inner, section.grid.clkhigh, #home-ad, .kakao_ad_area) { display: none !important; }
509
522
  html.dc-news-rows-off :is(table.gall_list tr.ub-content[data-type="icon_fnews"], .gall-detail-lst > li:has(.sp-lst-fnews)) { display: none !important; }
523
+ html.dc-comment-fab-off :is(#cmt_fab, .fab-wrap:has(> #cmt_fab)) { display: none !important; }
510
524
  html.dc-comment-ads-off :is(.comment_box li.ub-content.dory, .comment_box li.cmt_pw_ad, .all-comment-lst > li.dc-comment-boy) { display: none !important; }
511
525
  html.dc-silbe-alarm-off :is(.alarm-popup.silbe-alarm, .rtbAlarmPopup) { display: none !important; }
512
526
  html.dc-alarm-bubble :is(.alarm-popup.alarm-default, .alarmPopup) { display: none !important; }
@@ -1223,6 +1237,9 @@
1223
1237
  #dc-custom-modal .dc-btn.mini.id-type { color: var(--dc-type-id); }
1224
1238
  #dc-custom-modal .dc-btn.mini.nick-type { color: var(--dc-text-dim); }
1225
1239
 
1240
+ #dc-custom-modal .dc-btn:disabled { opacity: .35; cursor: default; }
1241
+ #dc-custom-modal .dc-btn.on-state { background: var(--dc-success); }
1242
+
1226
1243
  #dc-custom-modal .dc-btn.mini.on {
1227
1244
  border-color: var(--dc-danger);
1228
1245
  background: var(--dc-danger);
@@ -2030,6 +2047,25 @@
2030
2047
  font-weight: 700;
2031
2048
  }
2032
2049
 
2050
+ #dc-preview-backdrop {
2051
+ position: fixed;
2052
+ inset: 0;
2053
+ z-index: 99996;
2054
+ background: rgba(0,0,0,.18);
2055
+ -webkit-backdrop-filter: blur(14px) saturate(1.1);
2056
+ backdrop-filter: blur(14px) saturate(1.1);
2057
+ opacity: 0;
2058
+ visibility: hidden;
2059
+ transition: opacity .2s ease, visibility 0s linear .2s;
2060
+ }
2061
+
2062
+ html.dc-dark #dc-preview-backdrop { background: rgba(0,0,0,.4); }
2063
+ #dc-preview-backdrop.show { opacity: 1; visibility: visible; transition: opacity .2s ease, visibility 0s; }
2064
+
2065
+ #dc-preview.dc-pv-peek { transform: var(--dc-pv-from, translate(-50%, -50%) scale(.9)); transition: opacity .18s ease, transform .32s cubic-bezier(.2,.9,.3,1.15), visibility 0s linear .32s; }
2066
+ #dc-preview.dc-pv-peek.show { transform: translate(-50%, -50%); transition: opacity .18s ease, transform .32s cubic-bezier(.2,.9,.3,1.15), visibility 0s; }
2067
+ #dc-preview.dc-pv-peek { box-shadow: 0 24px 80px rgba(0,0,0,.35); }
2068
+
2033
2069
  .dc-pv-anchor { text-decoration: underline !important; text-decoration-color: var(--dc-accent-ink) !important; }
2034
2070
 
2035
2071
  .dc-key-cursor { outline: 2px solid var(--dc-accent-ink) !important; outline-offset: -2px; background: var(--dc-accent-soft) !important; }
@@ -3158,6 +3194,7 @@
3158
3194
  root.classList.toggle('dc-ad-slots-off', settings.hideAdSlots !== false);
3159
3195
  root.classList.toggle('dc-news-rows-off', settings.hideNewsRows !== false);
3160
3196
  root.classList.toggle('dc-comment-ads-off', settings.hideCommentAds !== false);
3197
+ root.classList.toggle('dc-comment-fab-off', settings.hideCommentFab !== false);
3161
3198
  root.classList.toggle('dc-silbe-alarm-off', settings.hideSilbeAlarm !== false);
3162
3199
  root.classList.toggle('dc-alarm-bubble', settings.alarmBubble !== false);
3163
3200
  root.classList.toggle('dc-hide-scrollbar', settings.autoHideScrollbar !== false && root.classList.contains('dc-classic-scrollbar'));
@@ -9507,7 +9544,10 @@
9507
9544
  link.addEventListener('click', () => closeBlockModal());
9508
9545
  actionsEl.appendChild(link);
9509
9546
  } else {
9510
- actionsEl.appendChild(makeButton(action.label, `mini ${action.className || ''}`, action.onClick));
9547
+ const btn = makeButton(action.label, `mini ${action.className || ''}`, action.onClick);
9548
+
9549
+ btn.disabled = !!action.disabled;
9550
+ actionsEl.appendChild(btn);
9511
9551
  }
9512
9552
  });
9513
9553
 
@@ -10405,6 +10445,14 @@
10405
10445
  off: '댓글돌이를 그대로 보여 줍니다.',
10406
10446
  refresh: 'root'
10407
10447
  },
10448
+ {
10449
+ group: 'clean',
10450
+ key: 'hideCommentFab',
10451
+ name: "떠 있는 '댓글로 이동' 버튼 숨기기 (모바일)",
10452
+ on: '글 화면 오른쪽 아래에 떠 있는 댓글 버튼을 숨깁니다. 본문 아래 [댓글] 링크와 차단 관리 버튼은 그대로입니다.',
10453
+ off: '댓글로 이동 버튼을 보여 줍니다.',
10454
+ refresh: 'root'
10455
+ },
10408
10456
  {
10409
10457
  group: 'clean',
10410
10458
  key: 'hideAdSlots',
@@ -10490,9 +10538,18 @@
10490
10538
  {
10491
10539
  group: 'list',
10492
10540
  key: 'hoverPreview',
10493
- name: '제목에 마우스를 올리면 글 미리보기 (PC)',
10494
- on: '목록 제목 위에 잠깐 머무르면 글 전체를 옆 창에 보여 줍니다. 창 안에서 스크롤해 읽고, 스크랩·작성자 차단도 바로 할 수 있습니다. 차단한 이미지·단어는 미리보기에서도 가립니다. 키보드로 고른 글은 P 로 엽니다.',
10495
- off: '제목을 눌러야 글을 봅니다.',
10541
+ name: '제목을 길게 누르면 글 미리보기 (PC)',
10542
+ on: '아이폰처럼 목록 제목을 길게 누르면 화면이 흐려지며 글 전체가 가운데에 떠오릅니다. 짧게 누르면 평소처럼 글로 갑니다. 창에서 본문을 누르면 글로 이동, 바깥을 누르거나 Esc 로 닫습니다. 스크랩·작성자 차단도 바로 할 수 있고, 차단한 이미지·단어는 미리보기에서도 가립니다. 키보드로 고른 글은 P 로 엽니다.',
10543
+ off: '미리보기를 쓰지 않습니다.',
10544
+ refresh: 'root'
10545
+ },
10546
+ {
10547
+ group: 'list',
10548
+ key: 'previewHover',
10549
+ parent: 'hoverPreview',
10550
+ name: '마우스를 올려 멈춰도 미리보기 (PC)',
10551
+ on: '제목 위에 마우스를 잠시 멈추면 누르지 않아도 미리보기가 뜹니다. 창 밖으로 마우스가 나가면 닫힙니다.',
10552
+ off: '길게 눌렀을 때만 미리보기가 뜹니다.',
10496
10553
  refresh: 'root'
10497
10554
  },
10498
10555
  {
@@ -11759,7 +11816,7 @@
11759
11816
  { key: 'words', label: '단어', fields: ['keywords', 'titles'] },
11760
11817
  { key: 'heads', label: '말머리', fields: ['heads'] },
11761
11818
  { key: 'scraps', label: '스크랩', fields: ['scraps'] },
11762
- { key: 'recent', label: '최근 갤러리', fields: ['recent'] },
11819
+ { key: 'recent', label: '최근 갤러리', fields: ['recent', 'recentOrder'] },
11763
11820
  { key: 'search', label: '검색 기록', fields: ['search'] },
11764
11821
  { key: 'settings', label: '설정', fields: ['settings'] }
11765
11822
  ];
@@ -11929,6 +11986,7 @@
11929
11986
  scraps: scraps.map(item => ({ ...item })),
11930
11987
  settings: syncedSettings(),
11931
11988
  recent: collectRecentForSync(),
11989
+ recentOrder: collectRecentOrderForSync(),
11932
11990
  search: collectSearchForSync(),
11933
11991
  hits: collectHitsForSync()
11934
11992
  });
@@ -12038,6 +12096,7 @@
12038
12096
  scraps: mergeScraps(local.scraps, base && base.scraps, remote && remote.scraps),
12039
12097
  settings: mergeSettings(local.settings, base && base.settings, remote && remote.settings),
12040
12098
  recent: mergeRecent(local.recent, base && base.recent, remote && remote.recent),
12099
+ recentOrder: mergeRecentOrder(local.recentOrder, remote && remote.recentOrder),
12041
12100
  search: mergeSearchHistory(local.search, base && base.search, remote && remote.search),
12042
12101
  hits: mergeBlockHits(local.hits, remote && remote.hits)
12043
12102
  };
@@ -12093,6 +12152,7 @@
12093
12152
  const data = collectSyncData();
12094
12153
 
12095
12154
  delete data.recent;
12155
+ delete data.recentOrder;
12096
12156
  delete data.search;
12097
12157
  delete data.hits;
12098
12158
 
@@ -12100,6 +12160,7 @@
12100
12160
  };
12101
12161
 
12102
12162
  const applySyncData = data => {
12163
+ const orderChanged = data && 'recentOrder' in data ? applyRecentOrderFromSync(data.recentOrder) : false;
12103
12164
  const recentChanged = data && 'recent' in data ? applyRecentFromSync(data.recent) : false;
12104
12165
  const searchChanged = data && 'search' in data ? applySearchFromSync(data.search) : false;
12105
12166
  const hitsChanged = data && 'hits' in data ? applyHitsFromSync(data.hits) : false;
@@ -12157,7 +12218,7 @@
12157
12218
  refreshAfterBlockChange();
12158
12219
  }
12159
12220
 
12160
- return changed || recentChanged || searchChanged || hitsChanged;
12221
+ return changed || recentChanged || orderChanged || searchChanged || hitsChanged;
12161
12222
  };
12162
12223
 
12163
12224
  const saveSyncUndo = (before, incoming, summary, from) => {
@@ -13401,6 +13462,38 @@
13401
13462
  };
13402
13463
  };
13403
13464
 
13465
+ const readRecentOrder = () => {
13466
+ const stored = readObject(STORAGE.recentOrder);
13467
+
13468
+ return {
13469
+ fixed: stored.fixed === true,
13470
+ keys: Array.isArray(stored.keys) ? stored.keys.filter(key => typeof key === 'string').slice(0, 200) : [],
13471
+ at: Number(stored.at) || 0
13472
+ };
13473
+ };
13474
+
13475
+ let recentOrder = readRecentOrder();
13476
+
13477
+ const saveRecentOrder = patch => {
13478
+ recentOrder = { ...recentOrder, ...patch, at: Date.now() };
13479
+ writeJson(STORAGE.recentOrder, recentOrder);
13480
+ };
13481
+
13482
+ const orderRecent = list => {
13483
+ const byTime = [...list].sort((a, b) => b.at - a.at);
13484
+
13485
+ if (!recentOrder.fixed) {
13486
+ return byTime;
13487
+ }
13488
+
13489
+ const index = new Map(recentOrder.keys.map((key, i) => [key, i]));
13490
+ const known = byTime.filter(item => index.has(recentKeyOf(item))).sort((a, b) => index.get(recentKeyOf(a)) - index.get(recentKeyOf(b)));
13491
+
13492
+ const fresh = byTime.filter(item => !index.has(recentKeyOf(item))).reverse();
13493
+
13494
+ return [...known, ...fresh];
13495
+ };
13496
+
13404
13497
  const tidyRecent = list => {
13405
13498
  const best = new Map();
13406
13499
 
@@ -13413,7 +13506,62 @@
13413
13506
  }
13414
13507
  });
13415
13508
 
13416
- return [...best.values()].sort((a, b) => b.at - a.at).slice(0, RECENT_LIMIT);
13509
+ const kept = [...best.values()].sort((a, b) => b.at - a.at).slice(0, RECENT_LIMIT);
13510
+ const ordered = orderRecent(kept);
13511
+
13512
+ if (recentOrder.fixed) {
13513
+ const keys = ordered.map(recentKeyOf);
13514
+
13515
+ if (JSON.stringify(keys) !== JSON.stringify(recentOrder.keys.filter(key => keys.includes(key)))) {
13516
+ recentOrder = { ...recentOrder, keys };
13517
+ writeJson(STORAGE.recentOrder, recentOrder);
13518
+ }
13519
+ }
13520
+
13521
+ return ordered;
13522
+ };
13523
+
13524
+ const setRecentFixed = fixed => {
13525
+ saveRecentOrder({ fixed, keys: recentGalls.map(recentKeyOf) });
13526
+ recentGalls = tidyRecent(recentGalls);
13527
+ writeJson(STORAGE.recent, recentGalls);
13528
+ exportNativeRecent();
13529
+ reorderNativeRecentDom();
13530
+ };
13531
+
13532
+ const moveRecent = (item, step) => {
13533
+ const list = [...recentGalls];
13534
+ const from = list.indexOf(item);
13535
+ const to = Math.max(0, Math.min(list.length - 1, from + step));
13536
+
13537
+ if (from < 0 || from === to) {
13538
+ return;
13539
+ }
13540
+
13541
+ list.splice(to, 0, ...list.splice(from, 1));
13542
+ saveRecentOrder({ fixed: true, keys: list.map(recentKeyOf) });
13543
+ recentGalls = tidyRecent(list);
13544
+ writeJson(STORAGE.recent, recentGalls);
13545
+ exportNativeRecent();
13546
+ reorderNativeRecentDom();
13547
+ };
13548
+
13549
+ const reorderNativeRecentDom = () => {
13550
+ if (!IS_PC_SITE || !recentOrder.fixed) {
13551
+ return;
13552
+ }
13553
+
13554
+ const index = new Map(recentGalls.map((item, i) => [item.id, i]));
13555
+
13556
+ document.querySelectorAll('#visit_history .vst_listbox, .visit_tablist .under_listbox, #visit_history_lyr .vst_list').forEach(box => {
13557
+ const items = [...box.children].filter(li => li.tagName === 'LI');
13558
+ const idOf = li => li.querySelector('[data-id]')?.getAttribute('data-id') || (/lately_gall_(\S+)/.exec(li.className) || [])[1] || '';
13559
+ const sorted = [...items].sort((a, b) => (index.has(idOf(a)) ? index.get(idOf(a)) : 999) - (index.has(idOf(b)) ? index.get(idOf(b)) : 999));
13560
+
13561
+ if (sorted.some((li, i) => li !== items[i])) {
13562
+ sorted.forEach(li => box.appendChild(li));
13563
+ }
13564
+ });
13417
13565
  };
13418
13566
 
13419
13567
  let recentGalls = tidyRecent(readArray(STORAGE.recent));
@@ -13636,6 +13784,31 @@
13636
13784
  saveRecent();
13637
13785
  };
13638
13786
 
13787
+ const watchNativeRecentDom = () => {
13788
+ if (!IS_PC_SITE) {
13789
+ return;
13790
+ }
13791
+
13792
+ const target = document.getElementById('visit_history');
13793
+
13794
+ if (!target) {
13795
+ return;
13796
+ }
13797
+
13798
+ let timer = null;
13799
+
13800
+ new MutationObserver(() => {
13801
+ if (!recentOrder.fixed) {
13802
+ return;
13803
+ }
13804
+
13805
+ clearTimeout(timer);
13806
+ timer = setTimeout(reorderNativeRecentDom, 60);
13807
+ }).observe(target, { childList: true, subtree: true });
13808
+
13809
+ reorderNativeRecentDom();
13810
+ };
13811
+
13639
13812
  const initRecentEarly = () => {
13640
13813
  importNativeRecent();
13641
13814
  exportNativeRecent();
@@ -13645,10 +13818,12 @@
13645
13818
  importNativeRecent();
13646
13819
  recordGalleryVisit();
13647
13820
  exportNativeRecent();
13821
+ watchNativeRecentDom();
13648
13822
 
13649
13823
  const follow = () => {
13650
- if (document.visibilityState === 'visible' && importNativeRecent()) {
13824
+ if (document.visibilityState === 'visible' && (importNativeRecent() || recentOrder.fixed)) {
13651
13825
  exportNativeRecent();
13826
+ reorderNativeRecentDom();
13652
13827
  }
13653
13828
  };
13654
13829
 
@@ -13658,6 +13833,31 @@
13658
13833
 
13659
13834
  const collectRecentForSync = () => recentGalls.map(item => ({ ...item }));
13660
13835
 
13836
+ const collectRecentOrderForSync = () => ({ ...recentOrder, keys: [...recentOrder.keys] });
13837
+
13838
+ const mergeRecentOrder = (local, remote) => {
13839
+ if (!remote || typeof remote !== 'object' || !Array.isArray(remote.keys)) {
13840
+ return local;
13841
+ }
13842
+
13843
+ return (Number(remote.at) || 0) > (Number(local && local.at) || 0) ? remote : local;
13844
+ };
13845
+
13846
+ const applyRecentOrderFromSync = data => {
13847
+ if (!data || typeof data !== 'object' || !Array.isArray(data.keys) || (Number(data.at) || 0) <= recentOrder.at) {
13848
+ return false;
13849
+ }
13850
+
13851
+ recentOrder = { fixed: data.fixed === true, keys: data.keys.filter(key => typeof key === 'string').slice(0, 200), at: Number(data.at) };
13852
+ writeJson(STORAGE.recentOrder, recentOrder);
13853
+ recentGalls = tidyRecent(recentGalls);
13854
+ writeJson(STORAGE.recent, recentGalls);
13855
+ exportNativeRecent();
13856
+ reorderNativeRecentDom();
13857
+
13858
+ return true;
13859
+ };
13860
+
13661
13861
  const mergeRecent = (local = [], base, remote) => {
13662
13862
  if (!Array.isArray(remote)) {
13663
13863
  return tidyRecent(local || []);
@@ -14246,18 +14446,35 @@
14246
14446
  return;
14247
14447
  }
14248
14448
 
14449
+ const orderRow = h('div', 'dc-btn-row');
14450
+
14451
+ orderRow.appendChild(makeButton(recentOrder.fixed ? '📌 순서 고정 중 · 끄기' : '📌 지금 순서로 고정', recentOrder.fixed ? 'wide on-state' : 'wide', () => {
14452
+ setRecentFixed(!recentOrder.fixed);
14453
+ showToast(recentOrder.fixed ? '📌 최근 방문 순서를 고정했습니다 · ↑↓ 로 바꿀 수 있어요' : '최근 방문 순서 고정을 풀었습니다 (방문한 순서로)');
14454
+ rerenderModal();
14455
+ }));
14456
+ area.appendChild(orderRow);
14457
+
14458
+ if (recentOrder.fixed) {
14459
+ area.appendChild(makeNote('고정한 순서는 디시 화면의 최근 방문 목록에도 그대로 보이고, 어느 갤러리를 가도 바뀌지 않습니다. 처음 가는 갤러리는 맨 뒤에 붙습니다.'));
14460
+ }
14461
+
14249
14462
  const filterRow = makeFilterRow(recentGalls.length);
14250
14463
 
14251
14464
  if (filterRow) {
14252
14465
  area.appendChild(filterRow);
14253
14466
  }
14254
14467
 
14255
- recentGalls.forEach(item => {
14468
+ recentGalls.forEach((item, i) => {
14256
14469
  area.appendChild(makeRow({
14257
14470
  title: item.n || item.id,
14258
14471
  chip: RECENT_KIND_LABEL[item.k] || '',
14259
14472
  meta: [item.id, item.at ? timeAgo(item.at) : ''].filter(Boolean).join(' · '),
14260
14473
  actions: [
14474
+ ...(recentOrder.fixed ? [
14475
+ { label: '↑', className: 'dc-icon-btn', onClick: () => { moveRecent(item, -1); rerenderModal(); }, disabled: i === 0 },
14476
+ { label: '↓', className: 'dc-icon-btn', onClick: () => { moveRecent(item, 1); rerenderModal(); }, disabled: i === recentGalls.length - 1 }
14477
+ ] : []),
14261
14478
  { label: '열기', href: recentUrl(item) },
14262
14479
  {
14263
14480
  label: '지우기',
@@ -14525,7 +14742,7 @@
14525
14742
 
14526
14743
  // =========================================================
14527
14744
 
14528
- const CLEANUP_IDLE_DAYS = 60;
14745
+ const CLEANUP_IDLE_DAYS = 7;
14529
14746
 
14530
14747
  const cleanupSuggestions = () => {
14531
14748
  flushBlockHits();
@@ -14762,7 +14979,11 @@
14762
14979
  '</div>' +
14763
14980
  '<div class="dc-pv-body"></div>';
14764
14981
  previewEl.addEventListener('mouseenter', () => clearTimeout(previewCloseTimer));
14765
- previewEl.addEventListener('mouseleave', () => schedulePreviewClose());
14982
+ previewEl.addEventListener('mouseleave', () => {
14983
+ if (previewMode === 'hover') {
14984
+ schedulePreviewClose();
14985
+ }
14986
+ });
14766
14987
  document.body.appendChild(previewEl);
14767
14988
 
14768
14989
  return previewEl;
@@ -14780,6 +15001,7 @@
14780
15001
 
14781
15002
  clearTimeout(previewCloseTimer);
14782
15003
  stopPreviewTheme();
15004
+ previewBackdrop?.classList.remove('show');
14783
15005
 
14784
15006
  if (previewEl) {
14785
15007
  previewEl.classList.remove('show');
@@ -14957,6 +15179,20 @@
14957
15179
  doc.getElementById('dc-pv-css').textContent = previewFrameCss();
14958
15180
  doc.body.appendChild(doc.importNode(body, true));
14959
15181
 
15182
+ doc.querySelectorAll('img').forEach(img => {
15183
+ let tries = 0;
15184
+ const original = img.getAttribute('src') || '';
15185
+
15186
+ img.addEventListener('error', () => {
15187
+ if (tries >= 4 || !/dcinside\.(com|co\.kr)\//.test(original)) {
15188
+ return;
15189
+ }
15190
+
15191
+ tries += 1;
15192
+ setTimeout(() => img.setAttribute('src', `${original}&${tries + 1}`), 250 * tries);
15193
+ });
15194
+ });
15195
+
14960
15196
  doc.querySelectorAll('video[data-src]').forEach(video => {
14961
15197
  const gif = video.getAttribute('data-src');
14962
15198
  let swapped = false;
@@ -15220,18 +15456,135 @@
15220
15456
  }
15221
15457
  };
15222
15458
 
15459
+ const PREVIEW_HOLD_MS = 350;
15460
+ let previewMode = 'peek';
15461
+ let previewBackdrop = null;
15462
+
15463
+ const ensurePreviewBackdrop = () => {
15464
+ if (previewBackdrop && previewBackdrop.isConnected) {
15465
+ return previewBackdrop;
15466
+ }
15467
+
15468
+ previewBackdrop = h('div', 'dc-ui');
15469
+ previewBackdrop.id = 'dc-preview-backdrop';
15470
+ previewBackdrop.addEventListener('mousedown', event => {
15471
+ event.preventDefault();
15472
+ event.stopPropagation();
15473
+ closePreview();
15474
+ });
15475
+ document.body.appendChild(previewBackdrop);
15476
+
15477
+ return previewBackdrop;
15478
+ };
15479
+
15480
+ const openPreview = (anchor, mode) => {
15481
+ previewMode = mode;
15482
+
15483
+ const el = ensurePreviewEl();
15484
+
15485
+ el.classList.toggle('dc-pv-peek', mode === 'peek');
15486
+
15487
+ try {
15488
+ const r = anchor.getBoundingClientRect();
15489
+ const ox = Math.round(r.left + Math.min(r.width, 240) / 2 - window.innerWidth / 2);
15490
+ const oy = Math.round(r.top + r.height / 2 - window.innerHeight / 2);
15491
+
15492
+ el.style.setProperty('--dc-pv-from', `translate(calc(-50% + ${ox}px), calc(-50% + ${oy}px)) scale(.35)`);
15493
+ } catch {
15494
+ el.style.removeProperty('--dc-pv-from');
15495
+ }
15496
+
15497
+ if (mode === 'peek') {
15498
+ ensurePreviewBackdrop().classList.add('show');
15499
+ } else {
15500
+ previewBackdrop?.classList.remove('show');
15501
+ }
15502
+
15503
+ showPreviewFor(anchor);
15504
+ };
15505
+
15223
15506
  const initHoverPreview = () => {
15224
15507
  if (!IS_PC_SITE) {
15225
15508
  return;
15226
15509
  }
15227
15510
 
15511
+ const hoverOn = () => previewEnabled() && settings.previewHover === true;
15512
+
15228
15513
  let hoverAnchor = null;
15229
15514
  let pressedAnchor = null;
15515
+ let holdTimer = null;
15516
+ let holdStart = null;
15517
+ let swallowClick = null;
15518
+
15519
+ document.addEventListener('mousedown', event => {
15520
+ const anchor = event.target instanceof Element ? event.target.closest(PREVIEW_TITLE_SELECTOR) : null;
15521
+
15522
+ if (!anchor || anchor.closest('.dc-ui')) {
15523
+ if (previewOpen() && !(event.target instanceof Node && previewEl.contains(event.target))) {
15524
+ closePreview();
15525
+ }
15526
+
15527
+ return;
15528
+ }
15529
+
15530
+ pressedAnchor = anchor;
15531
+ clearTimeout(previewHoverTimer);
15532
+
15533
+ if (!previewEnabled() || event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) {
15534
+ return;
15535
+ }
15536
+
15537
+ holdStart = { x: event.clientX, y: event.clientY };
15538
+ clearTimeout(holdTimer);
15539
+ holdTimer = setTimeout(() => {
15540
+ holdTimer = null;
15541
+ swallowClick = anchor;
15542
+
15543
+ window.getSelection?.()?.removeAllRanges?.();
15544
+ openPreview(anchor, 'peek');
15545
+ }, PREVIEW_HOLD_MS);
15546
+ }, true);
15547
+
15548
+ document.addEventListener('mousemove', event => {
15549
+ if (holdTimer && holdStart && Math.hypot(event.clientX - holdStart.x, event.clientY - holdStart.y) > 8) {
15550
+ clearTimeout(holdTimer);
15551
+ holdTimer = null;
15552
+ }
15553
+ }, { passive: true, capture: true });
15554
+
15555
+ const cancelHold = () => {
15556
+ clearTimeout(holdTimer);
15557
+ holdTimer = null;
15558
+ };
15559
+
15560
+ document.addEventListener('mouseup', cancelHold, true);
15561
+ document.addEventListener('dragstart', event => {
15562
+ if (swallowClick || holdTimer) {
15563
+ event.preventDefault();
15564
+ }
15565
+
15566
+ cancelHold();
15567
+ }, true);
15568
+
15569
+ document.addEventListener('click', event => {
15570
+ if (!swallowClick) {
15571
+ return;
15572
+ }
15573
+
15574
+ const anchor = event.target instanceof Element ? event.target.closest(PREVIEW_TITLE_SELECTOR) : null;
15575
+
15576
+ if (anchor === swallowClick) {
15577
+ event.preventDefault();
15578
+ event.stopImmediatePropagation();
15579
+ }
15580
+
15581
+ swallowClick = null;
15582
+ }, true);
15230
15583
 
15231
15584
  const armHover = anchor => {
15232
15585
  clearTimeout(previewHoverTimer);
15233
15586
 
15234
- if (!previewEnabled() || anchor === pressedAnchor) {
15587
+ if (!hoverOn() || anchor === pressedAnchor || (previewOpen() && previewMode === 'peek')) {
15235
15588
  return;
15236
15589
  }
15237
15590
 
@@ -15243,18 +15596,14 @@
15243
15596
  }
15244
15597
 
15245
15598
  clearTimeout(previewCloseTimer);
15246
- showPreviewFor(anchor);
15599
+ openPreview(anchor, 'hover');
15247
15600
  }, switching ? PREVIEW_SWITCH_DELAY : PREVIEW_DELAY);
15248
15601
  };
15249
15602
 
15250
15603
  document.addEventListener('mouseover', event => {
15251
15604
  const anchor = event.target instanceof Element ? event.target.closest(PREVIEW_TITLE_SELECTOR) : null;
15252
15605
 
15253
- if (!anchor || anchor.closest('.dc-ui')) {
15254
- return;
15255
- }
15256
-
15257
- if (anchor === hoverAnchor) {
15606
+ if (!anchor || anchor.closest('.dc-ui') || anchor === hoverAnchor) {
15258
15607
  return;
15259
15608
  }
15260
15609
 
@@ -15265,7 +15614,7 @@
15265
15614
  return;
15266
15615
  }
15267
15616
 
15268
- if (previewOpen()) {
15617
+ if (previewOpen() && previewMode === 'hover') {
15269
15618
  clearTimeout(previewCloseTimer);
15270
15619
  }
15271
15620
 
@@ -15282,15 +15631,6 @@
15282
15631
  }
15283
15632
  }, { passive: true });
15284
15633
 
15285
- document.addEventListener('mousedown', event => {
15286
- const anchor = event.target instanceof Element ? event.target.closest(PREVIEW_TITLE_SELECTOR) : null;
15287
-
15288
- if (anchor) {
15289
- pressedAnchor = anchor;
15290
- clearTimeout(previewHoverTimer);
15291
- }
15292
- }, true);
15293
-
15294
15634
  document.addEventListener('mouseout', event => {
15295
15635
  const anchor = event.target instanceof Element ? event.target.closest(PREVIEW_TITLE_SELECTOR) : null;
15296
15636
 
@@ -15308,19 +15648,13 @@
15308
15648
 
15309
15649
  clearTimeout(previewHoverTimer);
15310
15650
 
15311
- if (previewOpen() && !(event.relatedTarget instanceof Node && previewEl.contains(event.relatedTarget))) {
15651
+ if (previewOpen() && previewMode === 'hover' && !(event.relatedTarget instanceof Node && previewEl.contains(event.relatedTarget))) {
15312
15652
  schedulePreviewClose();
15313
15653
  }
15314
15654
  });
15315
15655
 
15316
- document.addEventListener('mousedown', event => {
15317
- if (previewOpen() && !(event.target instanceof Node && previewEl.contains(event.target))) {
15318
- closePreview();
15319
- }
15320
- }, true);
15321
-
15322
15656
  window.addEventListener('scroll', () => {
15323
- if (previewOpen() && !previewEl.matches(':hover')) {
15657
+ if (previewOpen() && previewMode === 'hover' && !previewEl.matches(':hover')) {
15324
15658
  closePreview();
15325
15659
  }
15326
15660
  }, { passive: true });
@@ -15356,7 +15690,7 @@
15356
15690
  return false;
15357
15691
  }
15358
15692
 
15359
- showPreviewFor(anchor);
15693
+ openPreview(anchor, 'peek');
15360
15694
  return true;
15361
15695
  };
15362
15696
  // =========================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kangsicj/dcinsideenhancer",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "description": "DCInside filter & enhancer userscript for mobile and PC (디시인사이드 차단·편의 유저스크립트)",
5
5
  "license": "MIT",
6
6
  "author": "kangsicj",