@kangsicj/dcinsideenhancer 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -62,6 +62,7 @@ One userscript for DCInside on mobile and PC: smart blocking, endless lists, ima
62
62
  - Gaps left by blocked posts are **refilled** from the next page
63
63
  - Come back from a post and you're **right where you left off**
64
64
  - 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
65
66
 
66
67
  </td>
67
68
  </tr>
@@ -77,7 +78,7 @@ One userscript for DCInside on mobile and PC: smart blocking, endless lists, ima
77
78
  <td valign="top">
78
79
 
79
80
  #### 🔄 Same setup on every device
80
- - Block lists, scraps, settings and **recently visited galleries** stay in sync automatically
81
+ - Block lists, scraps, settings, **recently visited galleries** and mobile search history stay in sync automatically
81
82
  - Link over the same Wi-Fi, with a 6-digit code or a link
82
83
  - Data is **encrypted on your device** — only you can read it
83
84
  - Get notified about incoming changes, with **undo**
package/README.md CHANGED
@@ -62,6 +62,7 @@
62
62
  - 차단으로 빈 자리는 다음 페이지 글로 **채워 둠**
63
63
  - 글을 보고 돌아오면 **보던 위치 그대로**
64
64
  - 읽은 글 색 정리, 맨 위로 버튼
65
+ - PC 키보드 단축키: **J/K** 다음·이전 글, Enter 열기, S 스크랩, / 검색
65
66
 
66
67
  </td>
67
68
  </tr>
@@ -77,7 +78,7 @@
77
78
  <td valign="top">
78
79
 
79
80
  #### 🔄 모든 기기에서 같은 설정
80
- - 차단 목록 · 스크랩 · 설정 · **최근 방문 갤러리**를 자동으로 맞춤
81
+ - 차단 목록 · 스크랩 · 설정 · **최근 방문 갤러리** · 모바일 검색 기록을 자동으로 맞춤
81
82
  - 같은 와이파이, 6자리 코드, 연결 링크로 간단 연결
82
83
  - 기기에서 **암호화**해 보내므로 내용은 나만 볼 수 있음
83
84
  - 받은 변경은 알림으로 알려 주고 **되돌리기** 가능
@@ -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.0
5
+ // @version 1.1.1
6
6
  // @description DCInside filter & enhancer — mobile & PC. Block users/words/heads/notices/images & GIFs across posts, infinite list, zoom viewer, theme sync, encrypted multi-device sync.
7
7
  // @description:ko 디시인사이드 차단·편의 기능 (모바일·PC) - 유저·단어(제목/본문)·말머리·공지 차단, 같은/비슷한 이미지·움짤 모든 글 차단, 디시콘 제거, 차단 미디어 데이터 절약, 목록 자동 보충·무한 스크롤·뒤로 가기 위치 복원, 스크랩, 이미지 확대뷰 개선, 야간모드 자동 연동, 암호화 기기 간 동기화
8
8
  // @author kangsicj
@@ -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.0
5
+ // @version 1.1.1
6
6
  // @description DCInside filter & enhancer — mobile & PC. Block users/words/heads/notices/images & GIFs across posts, infinite list, zoom viewer, theme sync, encrypted multi-device sync.
7
7
  // @description:ko 디시인사이드 차단·편의 기능 (모바일·PC) - 유저·단어(제목/본문)·말머리·공지 차단, 같은/비슷한 이미지·움짤 모든 글 차단, 디시콘 제거, 차단 미디어 데이터 절약, 목록 자동 보충·무한 스크롤·뒤로 가기 위치 복원, 스크랩, 이미지 확대뷰 개선, 야간모드 자동 연동, 암호화 기기 간 동기화
8
8
  // @author kangsicj
@@ -75,7 +75,7 @@
75
75
  // 0. 설정·상수
76
76
  // =========================================================
77
77
 
78
- const SCRIPT_VERSION = '1.1.0';
78
+ const SCRIPT_VERSION = '1.1.1';
79
79
  const PROJECT_URL = 'https://github.com/kangsicj/DCInsideEnhancer';
80
80
  const DONATE_URL = 'https://fairy.hada.io/@jinny#support';
81
81
 
@@ -107,6 +107,7 @@
107
107
  syncRt: 'dc_sync_rt',
108
108
  recent: 'dc_recent_galls',
109
109
  recentNative: 'dc_recent_native',
110
+ searchHistory: 'dc_search_history',
110
111
  alarmSeen: 'dc_alarm_marks',
111
112
  seenVersion: 'dc_seen_version'
112
113
  };
@@ -222,6 +223,7 @@
222
223
  hideSilbeAlarm: true,
223
224
  alarmBubble: true,
224
225
  alarmKeepPolling: true,
226
+ keyboardShortcuts: true,
225
227
  pcFitWidth: true,
226
228
  autoHideScrollbar: true,
227
229
  scrapIntercept: true,
@@ -275,6 +277,8 @@
275
277
  --dc-type-id: #6d538d;
276
278
  --dc-shadow: 0 8px 24px rgba(0,0,0,.16);
277
279
  --dc-shadow-sm: 0 2px 8px rgba(0,0,0,.12);
280
+ --dc-raised: #ffffff;
281
+ --dc-raised-shadow: 0 1px 2px rgba(0,0,0,.08), 0 2px 8px rgba(0,0,0,.08);
278
282
  --dc-overlay: rgba(0,0,0,.45);
279
283
  --dc-ghost-bg: rgba(128,128,128,.12);
280
284
  --dc-ghost-border: rgba(128,128,128,.38);
@@ -300,6 +304,8 @@
300
304
  --dc-type-id: #9c86b1;
301
305
  --dc-shadow: 0 8px 28px rgba(0,0,0,.55);
302
306
  --dc-shadow-sm: 0 2px 10px rgba(0,0,0,.45);
307
+ --dc-raised: #383b44;
308
+ --dc-raised-shadow: inset 0 0 0 1px rgba(255,255,255,.07), 0 1px 2px rgba(0,0,0,.3);
303
309
  --dc-overlay: rgba(0,0,0,.6);
304
310
  --dc-ghost-bg: rgba(150,150,150,.11);
305
311
  --dc-ghost-border: rgba(150,150,150,.30);
@@ -949,6 +955,10 @@
949
955
 
950
956
  #dc-custom-modal .dc-head-btn {
951
957
  flex: 0 0 auto;
958
+ display: inline-flex;
959
+ align-items: center;
960
+ justify-content: center;
961
+ box-sizing: border-box;
952
962
  height: 30px;
953
963
  padding: 0 12px;
954
964
  border: 1px solid var(--dc-border);
@@ -957,7 +967,7 @@
957
967
  color: var(--dc-text-dim);
958
968
  font-size: 12px;
959
969
  font-weight: 700;
960
- line-height: 28px;
970
+ line-height: 1;
961
971
  cursor: pointer;
962
972
  }
963
973
 
@@ -978,7 +988,15 @@
978
988
 
979
989
  #dc-custom-modal .dc-modal-tab {
980
990
  flex: 1 1 0;
991
+ display: flex;
992
+ align-items: center;
993
+ justify-content: center;
994
+ gap: 3px;
995
+ min-width: 0;
996
+ box-sizing: border-box;
981
997
  height: 36px;
998
+ margin: 0;
999
+ line-height: 1;
982
1000
  padding: 0 2px;
983
1001
  border: none;
984
1002
  border-radius: 9px;
@@ -992,11 +1010,14 @@
992
1010
  }
993
1011
 
994
1012
  #dc-custom-modal .dc-modal-tab.active {
995
- background: var(--dc-surface);
1013
+ background: var(--dc-raised);
996
1014
  color: var(--dc-accent-ink);
997
- box-shadow: var(--dc-shadow-sm);
1015
+ box-shadow: var(--dc-raised-shadow);
998
1016
  }
999
1017
 
1018
+ #dc-custom-modal .dc-modal-tab .dc-tab-ico { flex: 0 0 auto; font-size: 13px; line-height: 1; }
1019
+ #dc-custom-modal .dc-modal-tab .dc-tab-txt { min-width: 0; overflow: hidden; text-overflow: ellipsis; line-height: 1.2; }
1020
+
1000
1021
  #dc-custom-modal .dc-section-title {
1001
1022
  display: flex;
1002
1023
  align-items: center;
@@ -1830,6 +1851,8 @@
1830
1851
 
1831
1852
  #dc-custom-modal .dc-row.dc-row-new .dc-row-title { font-weight: 800; }
1832
1853
 
1854
+ .dc-key-cursor { outline: 2px solid var(--dc-accent-ink) !important; outline-offset: -2px; background: var(--dc-accent-soft) !important; }
1855
+
1833
1856
  #dc-custom-modal .dc-about {
1834
1857
  margin-top: 14px;
1835
1858
  padding: 12px 13px;
@@ -3234,7 +3257,7 @@
3234
3257
 
3235
3258
  window.addEventListener('beforeunload', noteNetLeaving);
3236
3259
 
3237
- document.addEventListener('click', event => {
3260
+ window.addEventListener('click', event => {
3238
3261
  const link = event.target instanceof Element ? event.target.closest('a[href]') : null;
3239
3262
 
3240
3263
  if (!link || event.defaultPrevented || event.button > 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
@@ -3253,7 +3276,7 @@
3253
3276
  }
3254
3277
  } catch {
3255
3278
  }
3256
- }, true);
3279
+ });
3257
3280
 
3258
3281
  try {
3259
3282
  if (window.navigation && typeof window.navigation.addEventListener === 'function') {
@@ -5823,6 +5846,38 @@
5823
5846
  // =========================================================
5824
5847
 
5825
5848
  let toastTimer = null;
5849
+ const toastListeners = [];
5850
+
5851
+ const notifyToastListeners = () => toastListeners.forEach(fn => {
5852
+ try {
5853
+ fn();
5854
+ } catch {
5855
+ }
5856
+ });
5857
+
5858
+ const placeToast = toast => {
5859
+ toast.style.bottom = '';
5860
+
5861
+ const fab = document.getElementById('dc-custom-fab');
5862
+
5863
+ if (!fab || getComputedStyle(fab).display === 'none') {
5864
+ return;
5865
+ }
5866
+
5867
+ const f = fab.getBoundingClientRect();
5868
+ const t = toast.getBoundingClientRect();
5869
+
5870
+ if (!f.width || !t.width) {
5871
+ return;
5872
+ }
5873
+
5874
+ const overlapX = t.left < f.right + 6 && t.right > f.left - 6;
5875
+ const overlapY = t.top < f.bottom + 6 && t.bottom > f.top - 6;
5876
+
5877
+ if (overlapX && overlapY) {
5878
+ toast.style.bottom = `${Math.round(window.innerHeight - f.top + 10)}px`;
5879
+ }
5880
+ };
5826
5881
 
5827
5882
  const showToast = (text, { actionLabel = '', onAction = null, duration = 3500 } = {}) => {
5828
5883
  let toast = document.getElementById('dc-toast');
@@ -5851,8 +5906,13 @@
5851
5906
  };
5852
5907
 
5853
5908
  toast.classList.add('show');
5909
+ placeToast(toast);
5910
+ notifyToastListeners();
5854
5911
  clearTimeout(toastTimer);
5855
- toastTimer = setTimeout(() => toast.classList.remove('show'), duration);
5912
+ toastTimer = setTimeout(() => {
5913
+ toast.classList.remove('show');
5914
+ setTimeout(notifyToastListeners, 220);
5915
+ }, duration);
5856
5916
  };
5857
5917
 
5858
5918
  const offerUndo = (text, undo) => {
@@ -10138,6 +10198,14 @@
10138
10198
  off: '모든 댓글을 보여 줍니다.',
10139
10199
  refresh: 'all'
10140
10200
  },
10201
+ {
10202
+ group: 'list',
10203
+ key: 'keyboardShortcuts',
10204
+ name: '키보드 단축키 (PC)',
10205
+ on: '목록에서 J/K 로 글을 고르고 Enter 로 엽니다. 글에서는 J/K 로 다음·이전 글, S 스크랩, / 검색창, B 차단 관리, ? 안내.',
10206
+ off: '단축키를 쓰지 않습니다.',
10207
+ refresh: 'none'
10208
+ },
10141
10209
  {
10142
10210
  group: 'list',
10143
10211
  key: 'autoFillList',
@@ -11295,7 +11363,11 @@
11295
11363
  '<button id="dc-modal-close-btn" class="dc-head-btn" type="button">닫기</button>' +
11296
11364
  '</div>' +
11297
11365
  `<div class="dc-modal-tabs">${Object.keys(MODAL_VIEWS)
11298
- .map(key => `<button class="dc-modal-tab" type="button" data-dc-view="${key}">${MODAL_VIEWS[key].tab}</button>`)
11366
+ .map(key => {
11367
+ const [ico, ...rest] = MODAL_VIEWS[key].tab.split(' ');
11368
+
11369
+ return `<button class="dc-modal-tab" type="button" data-dc-view="${key}"><span class="dc-tab-ico" aria-hidden="true">${ico}</span><span class="dc-tab-txt">${rest.join(' ')}</span></button>`;
11370
+ })
11299
11371
  .join('')}</div>` +
11300
11372
  '</div>' +
11301
11373
  '<div id="dc-modal-content"></div>';
@@ -11385,6 +11457,7 @@
11385
11457
  { key: 'heads', label: '말머리', fields: ['heads'] },
11386
11458
  { key: 'scraps', label: '스크랩', fields: ['scraps'] },
11387
11459
  { key: 'recent', label: '최근 갤러리', fields: ['recent'] },
11460
+ { key: 'search', label: '검색 기록', fields: ['search'] },
11388
11461
  { key: 'settings', label: '설정', fields: ['settings'] }
11389
11462
  ];
11390
11463
 
@@ -11552,7 +11625,8 @@
11552
11625
  heads: JSON.parse(JSON.stringify(hiddenHeadMap || {})),
11553
11626
  scraps: scraps.map(item => ({ ...item })),
11554
11627
  settings: syncedSettings(),
11555
- recent: collectRecentForSync()
11628
+ recent: collectRecentForSync(),
11629
+ search: collectSearchForSync()
11556
11630
  });
11557
11631
 
11558
11632
  const skippedFields = skip => SYNC_PARTS.filter(part => skip.includes(part.key)).flatMap(part => part.fields);
@@ -11659,7 +11733,8 @@
11659
11733
  heads: mergeHeads(local.heads, base && base.heads, remote && remote.heads),
11660
11734
  scraps: mergeScraps(local.scraps, base && base.scraps, remote && remote.scraps),
11661
11735
  settings: mergeSettings(local.settings, base && base.settings, remote && remote.settings),
11662
- recent: mergeRecent(local.recent, base && base.recent, remote && remote.recent)
11736
+ recent: mergeRecent(local.recent, base && base.recent, remote && remote.recent),
11737
+ search: mergeSearchHistory(local.search, base && base.search, remote && remote.search)
11663
11738
  };
11664
11739
 
11665
11740
  skippedFields(skip).forEach(field => {
@@ -11713,12 +11788,14 @@
11713
11788
  const data = collectSyncData();
11714
11789
 
11715
11790
  delete data.recent;
11791
+ delete data.search;
11716
11792
 
11717
11793
  return JSON.stringify(data);
11718
11794
  };
11719
11795
 
11720
11796
  const applySyncData = data => {
11721
11797
  const recentChanged = data && 'recent' in data ? applyRecentFromSync(data.recent) : false;
11798
+ const searchChanged = data && 'search' in data ? applySearchFromSync(data.search) : false;
11722
11799
  const clean = sanitizeBackup({ app: BACKUP_APP, ...data });
11723
11800
  const before = blockSyncData();
11724
11801
 
@@ -11773,7 +11850,7 @@
11773
11850
  refreshAfterBlockChange();
11774
11851
  }
11775
11852
 
11776
- return changed || recentChanged;
11853
+ return changed || recentChanged || searchChanged;
11777
11854
  };
11778
11855
 
11779
11856
  const saveSyncUndo = (before, incoming, summary, from) => {
@@ -13134,8 +13211,12 @@
13134
13211
  const nativeKeys = new Set(native.map(recentKeyOf));
13135
13212
  const snapshot = readArray(STORAGE.recentNative).filter(key => typeof key === 'string');
13136
13213
 
13137
- if (snapshot.length && native.length < RECENT_NATIVE_CAP - 1) {
13138
- const removed = new Set(snapshot.filter(key => !nativeKeys.has(key)));
13214
+ if (snapshot.length) {
13215
+ const snapSet = new Set(snapshot);
13216
+ const added = native.filter(item => !snapSet.has(recentKeyOf(item))).length;
13217
+ const overflow = Math.max(0, snapshot.length + added - RECENT_NATIVE_CAP);
13218
+ const kept = snapshot.slice(0, snapshot.length - overflow);
13219
+ const removed = new Set(kept.filter(key => !nativeKeys.has(key)));
13139
13220
 
13140
13221
  if (removed.size) {
13141
13222
  recentGalls = recentGalls.filter(item => !removed.has(recentKeyOf(item)));
@@ -13489,24 +13570,40 @@
13489
13570
  }
13490
13571
 
13491
13572
  const fab = document.getElementById('dc-custom-fab');
13492
- const rect = fab && fab.offsetParent !== null ? fab.getBoundingClientRect() : null;
13573
+ const rect = fab && getComputedStyle(fab).display !== 'none' ? fab.getBoundingClientRect() : null;
13493
13574
  const vw = window.innerWidth;
13494
13575
  const vh = window.innerHeight;
13576
+ let bottom = 144;
13495
13577
 
13496
13578
  if (rect && rect.width) {
13497
13579
  const onRight = rect.left + rect.width / 2 > vw / 2;
13498
13580
 
13499
13581
  bubbleBox.style.left = onRight ? '' : `${Math.max(8, rect.left)}px`;
13500
13582
  bubbleBox.style.right = onRight ? `${Math.max(8, vw - rect.right)}px` : '';
13501
- bubbleBox.style.bottom = `${Math.max(8, vh - rect.top + 10)}px`;
13583
+ bottom = Math.max(8, vh - rect.top + 10);
13502
13584
  bubbleBox.classList.toggle('dc-left', !onRight);
13503
13585
  } else {
13504
13586
  bubbleBox.style.left = '';
13505
13587
  bubbleBox.style.right = '15px';
13506
- bubbleBox.style.bottom = 'calc(144px + env(safe-area-inset-bottom))';
13588
+ bubbleBox.classList.remove('dc-left');
13589
+ }
13590
+
13591
+ const toast = document.getElementById('dc-toast');
13592
+
13593
+ if (toast && toast.classList.contains('show')) {
13594
+ const t = toast.getBoundingClientRect();
13595
+ const box = bubbleBox.getBoundingClientRect();
13596
+
13597
+ if (t.width && t.left < box.right && t.right > box.left) {
13598
+ bottom = Math.max(bottom, vh - t.top + 8);
13599
+ }
13507
13600
  }
13601
+
13602
+ bubbleBox.style.bottom = `${Math.round(bottom)}px`;
13508
13603
  };
13509
13604
 
13605
+ toastListeners.push(() => placeBubbleBox());
13606
+
13510
13607
  const ensureBubbleBox = () => {
13511
13608
  if (bubbleBox && bubbleBox.isConnected) {
13512
13609
  return bubbleBox;
@@ -13730,9 +13827,24 @@
13730
13827
  }
13731
13828
  };
13732
13829
 
13733
- const initSiteAlarms = () => {
13734
- if (alarmWatchTimer || !alarmStorageKeys().length && !siteUserId()) {
13830
+ function initSiteAlarms() {
13831
+ if (alarmWatchTimer || !(IS_MOBILE_SITE || /^gall\.dcinside\.com$/i.test(location.hostname))) {
13832
+ paintAlarmBadge();
13833
+ return;
13834
+ }
13835
+
13836
+ if (!siteUserId() && !alarmStorageKeys().length) {
13735
13837
  paintAlarmBadge();
13838
+
13839
+ if (!initSiteAlarms.retried) {
13840
+ initSiteAlarms.retried = true;
13841
+ [3000, 10000].forEach(delay => setTimeout(() => {
13842
+ if (!alarmWatchTimer && (siteUserId() || alarmStorageKeys().length)) {
13843
+ initSiteAlarms();
13844
+ }
13845
+ }, delay));
13846
+ }
13847
+
13736
13848
  return;
13737
13849
  }
13738
13850
 
@@ -13873,6 +13985,180 @@
13873
13985
  })();
13874
13986
 
13875
13987
  initRecentEarly();
13988
+
13989
+ // =========================================================
13990
+
13991
+ // =========================================================
13992
+
13993
+ const KEY_HELP = 'J/K 다음·이전 글 · Enter 열기 · S 스크랩 · / 검색 · B 차단 관리';
13994
+
13995
+ const typingTarget = el =>
13996
+ !!el && (el.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) || !!el.closest?.('[contenteditable="true"]'));
13997
+
13998
+ const visibleListRows = () => {
13999
+ const listEl = pickListElement(document);
14000
+
14001
+ if (!listEl) {
14002
+ return [];
14003
+ }
14004
+
14005
+ return realListItems(listEl).filter(row => row.getClientRects().length && listItemKey(row) && !row.closest('.dc-ui'));
14006
+ };
14007
+
14008
+ let keyCursor = null;
14009
+
14010
+ const moveKeyCursor = step => {
14011
+ const rows = visibleListRows();
14012
+
14013
+ if (!rows.length) {
14014
+ return false;
14015
+ }
14016
+
14017
+ const here = currentPostKey();
14018
+ let index = keyCursor ? rows.indexOf(keyCursor) : -1;
14019
+
14020
+ if (currentPostInfo()) {
14021
+ const at = rows.findIndex(row => listItemKey(row) === here);
14022
+ const target = at >= 0 ? rows[at + step] : step > 0 ? rows[0] : null;
14023
+ const link = target && target.querySelector(POST_LINK_SELECTOR);
14024
+
14025
+ if (link) {
14026
+ location.href = link.href;
14027
+ return true;
14028
+ }
14029
+
14030
+ showToast(step > 0 ? '아래에 더 볼 글이 없습니다' : '첫 글입니다');
14031
+ return true;
14032
+ }
14033
+
14034
+ if (index < 0) {
14035
+ index = rows.findIndex(row => row.getBoundingClientRect().bottom > 60);
14036
+ index = Math.max(0, step > 0 ? index : index);
14037
+ } else {
14038
+ index = Math.min(rows.length - 1, Math.max(0, index + step));
14039
+ }
14040
+
14041
+ keyCursor?.classList.remove('dc-key-cursor');
14042
+ keyCursor = rows[index];
14043
+ keyCursor.classList.add('dc-key-cursor');
14044
+ keyCursor.scrollIntoView({ block: 'nearest' });
14045
+
14046
+ return true;
14047
+ };
14048
+
14049
+ const initKeyboardShortcuts = () => {
14050
+ document.addEventListener('keydown', event => {
14051
+ if (settings.keyboardShortcuts === false || event.defaultPrevented || event.ctrlKey || event.metaKey || event.altKey) {
14052
+ return;
14053
+ }
14054
+
14055
+ if (typingTarget(event.target) || typingTarget(document.activeElement)) {
14056
+ return;
14057
+ }
14058
+
14059
+ if (document.getElementById('dc-custom-modal')?.classList.contains('show')) {
14060
+ return;
14061
+ }
14062
+
14063
+ const key = event.key;
14064
+ let handled = false;
14065
+
14066
+ if (key === 'j' || key === 'J') {
14067
+ handled = moveKeyCursor(1);
14068
+ } else if (key === 'k' || key === 'K') {
14069
+ handled = moveKeyCursor(-1);
14070
+ } else if ((key === 'Enter' || key === 'o' || key === 'O') && keyCursor && keyCursor.isConnected) {
14071
+ const link = keyCursor.querySelector(POST_LINK_SELECTOR);
14072
+
14073
+ if (link) {
14074
+ location.href = link.href;
14075
+ handled = true;
14076
+ }
14077
+ } else if (key === 's' || key === 'S') {
14078
+ const info = currentPostInfo();
14079
+
14080
+ if (info) {
14081
+ toggleScrap(info);
14082
+ handled = true;
14083
+ }
14084
+ } else if (key === '/') {
14085
+ const box = document.querySelector('.in_keyword, #preSWord, input.js-keyword, input[name="search_keyword"], .top_search input[type="text"]');
14086
+
14087
+ if (box) {
14088
+ box.focus();
14089
+ handled = true;
14090
+ }
14091
+ } else if (key === 'b' || key === 'B') {
14092
+ openBlockModal(document.getElementById('dc-custom-fab')?.dataset.dcAlarm ? 'recent' : 'overview');
14093
+ handled = true;
14094
+ } else if (key === '?') {
14095
+ showToast(`⌨️ ${KEY_HELP}`, { duration: 6000 });
14096
+ handled = true;
14097
+ }
14098
+
14099
+ if (handled) {
14100
+ event.preventDefault();
14101
+ }
14102
+ });
14103
+ };
14104
+
14105
+ // =========================================================
14106
+
14107
+ // =========================================================
14108
+
14109
+ const SEARCH_HISTORY_MAX = 30;
14110
+
14111
+ const readSiteSearchHistory = () => {
14112
+ if (!IS_MOBILE_SITE) {
14113
+ return null;
14114
+ }
14115
+
14116
+ try {
14117
+ const list = JSON.parse(localStorage.getItem('searchHistory') || '[]');
14118
+
14119
+ return Array.isArray(list) ? list.filter(item => typeof item === 'string' && item.trim()).map(item => item.slice(0, 60)) : [];
14120
+ } catch {
14121
+ return [];
14122
+ }
14123
+ };
14124
+
14125
+ const collectSearchForSync = () => {
14126
+ const site = readSiteSearchHistory();
14127
+ const list = site || readArray(STORAGE.searchHistory).filter(item => typeof item === 'string');
14128
+
14129
+ return [...new Set(list)].slice(-SEARCH_HISTORY_MAX);
14130
+ };
14131
+
14132
+ const mergeSearchHistory = (local = [], base, remote) => {
14133
+ if (!Array.isArray(remote)) {
14134
+ return [...new Set(local || [])].slice(-SEARCH_HISTORY_MAX);
14135
+ }
14136
+
14137
+ return mergeList(local || [], Array.isArray(base) ? base : null, remote).slice(-SEARCH_HISTORY_MAX);
14138
+ };
14139
+
14140
+ const applySearchFromSync = list => {
14141
+ if (!Array.isArray(list)) {
14142
+ return false;
14143
+ }
14144
+
14145
+ const next = [...new Set(list.filter(item => typeof item === 'string'))].slice(-SEARCH_HISTORY_MAX);
14146
+
14147
+ if (JSON.stringify(next) === JSON.stringify(collectSearchForSync())) {
14148
+ return false;
14149
+ }
14150
+
14151
+ writeJson(STORAGE.searchHistory, next);
14152
+
14153
+ if (IS_MOBILE_SITE) {
14154
+ try {
14155
+ localStorage.setItem('searchHistory', JSON.stringify(next));
14156
+ } catch {
14157
+ }
14158
+ }
14159
+
14160
+ return true;
14161
+ };
13876
14162
  // =========================================================
13877
14163
  // 14. 플로팅 버튼
13878
14164
  // =========================================================
@@ -14860,6 +15146,7 @@
14860
15146
  initSync();
14861
15147
  initRecent();
14862
15148
  initSiteAlarms();
15149
+ initKeyboardShortcuts();
14863
15150
  restoreListSnapshot();
14864
15151
  processElements();
14865
15152
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kangsicj/dcinsideenhancer",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "DCInside filter & enhancer userscript for mobile and PC (디시인사이드 차단·편의 유저스크립트)",
5
5
  "license": "MIT",
6
6
  "author": "kangsicj",