@kangsicj/dcinsideenhancer 1.3.0 → 1.3.2

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.
@@ -2,7 +2,7 @@
2
2
  // @name DCInside Enhancer
3
3
  // @name:ko 디시 인핸서
4
4
  // @namespace https://github.com/kangsicj/DCInsideEnhancer
5
- // @version 1.3.0
5
+ // @version 1.3.2
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.3.0
5
+ // @version 1.3.2
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.3.0';
78
+ const SCRIPT_VERSION = '1.3.2';
79
79
  const PROJECT_URL = 'https://github.com/kangsicj/DCInsideEnhancer';
80
80
  const DONATE_URL = 'https://fairy.hada.io/@jinny#support';
81
81
 
@@ -223,6 +223,8 @@
223
223
  hideNewsRows: true,
224
224
  hideCommentAds: true,
225
225
  hideCommentFab: true,
226
+ mobileRecentBar: true,
227
+ mobileRecentStrip: true,
226
228
  hideSilbeAlarm: true,
227
229
  alarmBubble: true,
228
230
  alarmKeepPolling: true,
@@ -2068,6 +2070,96 @@
2068
2070
 
2069
2071
  .dc-pv-anchor { text-decoration: underline !important; text-decoration-color: var(--dc-accent-ink) !important; }
2070
2072
 
2073
+ #dc-recent-bar {
2074
+ display: flex;
2075
+ align-items: center;
2076
+ gap: 0;
2077
+ height: 34px;
2078
+ margin: 0;
2079
+ padding: 0 6px;
2080
+ overflow-x: auto;
2081
+ overflow-y: hidden;
2082
+ -webkit-overflow-scrolling: touch;
2083
+ scrollbar-width: none;
2084
+ overscroll-behavior-x: contain;
2085
+ background: var(--dc-rb-bg, #3b4890);
2086
+ border-bottom: 1px solid rgba(255,255,255,.12);
2087
+ box-sizing: border-box;
2088
+ }
2089
+ #dc-recent-bar::-webkit-scrollbar { display: none; }
2090
+ #dc-recent-bar .dc-rb-item {
2091
+ flex: 0 0 auto;
2092
+ display: inline-flex;
2093
+ align-items: center;
2094
+ gap: 2px;
2095
+ max-width: 46vw;
2096
+ height: 34px;
2097
+ padding: 0 10px;
2098
+ overflow: hidden;
2099
+ color: rgba(255,255,255,.82);
2100
+ font-size: 13px;
2101
+ font-weight: 500;
2102
+ letter-spacing: -.3px;
2103
+ text-decoration: none;
2104
+ text-overflow: ellipsis;
2105
+ white-space: nowrap;
2106
+ }
2107
+ #dc-recent-bar .dc-rb-item + .dc-rb-item { border-left: 1px solid rgba(255,255,255,.18); }
2108
+ #dc-recent-bar .dc-rb-item.on { color: var(--dc-rb-on, #ffe10a); font-weight: 700; }
2109
+ #dc-recent-bar .dc-rb-kind { font-size: 11px; opacity: .8; }
2110
+
2111
+ html.dc-recent-strip-on #lately_pop.dc-strip-lately #swiper-lately,
2112
+ html.dc-recent-strip-on #lately_pop.dc-strip-lately #lately_paging1 { display: none !important; }
2113
+ .dc-recent-strip {
2114
+ display: flex;
2115
+ gap: 8px;
2116
+ padding: 12px 12px 14px;
2117
+ overflow-x: auto;
2118
+ overflow-y: hidden;
2119
+ -webkit-overflow-scrolling: touch;
2120
+ scroll-snap-type: x proximity;
2121
+ scrollbar-width: none;
2122
+ overscroll-behavior-x: contain;
2123
+ }
2124
+ .dc-recent-strip::-webkit-scrollbar { display: none; }
2125
+ .dc-recent-strip[hidden] { display: none !important; }
2126
+ .dc-recent-strip .dc-rs-chip {
2127
+ flex: 0 0 auto;
2128
+ display: inline-flex;
2129
+ align-items: center;
2130
+ gap: 4px;
2131
+ max-width: 60vw;
2132
+ height: 36px;
2133
+ padding: 0 4px 0 14px;
2134
+ border: 1px solid var(--dc-border);
2135
+ border-radius: 999px;
2136
+ background: var(--dc-surface-2);
2137
+ color: var(--dc-text);
2138
+ font-size: 14px;
2139
+ font-weight: 600;
2140
+ text-decoration: none;
2141
+ white-space: nowrap;
2142
+ scroll-snap-align: start;
2143
+ }
2144
+ .dc-recent-strip .dc-rs-chip.here { border-color: var(--dc-accent-ink); color: var(--dc-accent-ink); background: var(--dc-accent-soft); }
2145
+ .dc-recent-strip .dc-rs-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
2146
+ .dc-recent-strip .dc-rs-kind { color: var(--dc-text-dim); font-size: 13px; }
2147
+ .dc-recent-strip .dc-rs-del {
2148
+ flex: 0 0 auto;
2149
+ width: 28px;
2150
+ height: 28px;
2151
+ margin: 0;
2152
+ padding: 0;
2153
+ border: none;
2154
+ border-radius: 50%;
2155
+ background: transparent;
2156
+ color: var(--dc-text-dim);
2157
+ font-size: 18px;
2158
+ line-height: 28px;
2159
+ }
2160
+ .dc-recent-strip .dc-rs-pin,
2161
+ .dc-recent-strip .dc-rs-empty { flex: 0 0 auto; align-self: center; padding: 0 6px; color: var(--dc-text-dim); font-size: 12px; white-space: nowrap; }
2162
+
2071
2163
  .dc-key-cursor { outline: 2px solid var(--dc-accent-ink) !important; outline-offset: -2px; background: var(--dc-accent-soft) !important; }
2072
2164
 
2073
2165
  #dc-custom-modal .dc-about {
@@ -10453,6 +10545,22 @@
10453
10545
  off: '댓글로 이동 버튼을 보여 줍니다.',
10454
10546
  refresh: 'root'
10455
10547
  },
10548
+ {
10549
+ group: 'clean',
10550
+ key: 'mobileRecentBar',
10551
+ name: '메뉴 위에 최근 방문 갤러리 늘 보이기 (모바일)',
10552
+ on: '검색창과 [갤러리 · 마이너갤 · …] 메뉴 사이에 최근 방문 갤러리를 한 줄로 보여 줍니다. 옆으로 밀어 보고, 누르면 바로 이동합니다. 지금 갤러리는 노란색입니다.',
10553
+ off: '[최근 방문]을 눌러야 보입니다.',
10554
+ refresh: 'root'
10555
+ },
10556
+ {
10557
+ group: 'clean',
10558
+ key: 'mobileRecentStrip',
10559
+ name: "'최근 방문'을 한 줄로 밀어 보기 (모바일)",
10560
+ on: '위쪽 [최근 방문]을 누르면 넓은 표 대신 갤러리가 순서대로 한 줄에 나오고, 옆으로 밀어 봅니다. 고정한 순서를 그대로 따릅니다.',
10561
+ off: '사이트 기본 표(2줄 × 4칸, 넘겨 보기)로 보여 줍니다.',
10562
+ refresh: 'root'
10563
+ },
10456
10564
  {
10457
10565
  group: 'clean',
10458
10566
  key: 'hideAdSlots',
@@ -10745,6 +10853,14 @@
10745
10853
  repinReadColors();
10746
10854
  }
10747
10855
 
10856
+ if (item.key === 'mobileRecentBar') {
10857
+ renderRecentBar();
10858
+ }
10859
+
10860
+ if (item.key === 'mobileRecentStrip') {
10861
+ renderRecentStrip();
10862
+ }
10863
+
10748
10864
  if (item.key === 'alarmBubble') {
10749
10865
  paintAlarmBadge();
10750
10866
  }
@@ -13510,10 +13626,11 @@
13510
13626
  const ordered = orderRecent(kept);
13511
13627
 
13512
13628
  if (recentOrder.fixed) {
13513
- const keys = ordered.map(recentKeyOf);
13629
+ const known = new Set(recentOrder.keys);
13630
+ const added = ordered.map(recentKeyOf).filter(key => !known.has(key));
13514
13631
 
13515
- if (JSON.stringify(keys) !== JSON.stringify(recentOrder.keys.filter(key => keys.includes(key)))) {
13516
- recentOrder = { ...recentOrder, keys };
13632
+ if (added.length) {
13633
+ recentOrder = { ...recentOrder, keys: [...recentOrder.keys, ...added].slice(-200) };
13517
13634
  writeJson(STORAGE.recentOrder, recentOrder);
13518
13635
  }
13519
13636
  }
@@ -13527,6 +13644,7 @@
13527
13644
  writeJson(STORAGE.recent, recentGalls);
13528
13645
  exportNativeRecent();
13529
13646
  reorderNativeRecentDom();
13647
+ refreshRecentViews();
13530
13648
  };
13531
13649
 
13532
13650
  const moveRecent = (item, step) => {
@@ -13544,6 +13662,7 @@
13544
13662
  writeJson(STORAGE.recent, recentGalls);
13545
13663
  exportNativeRecent();
13546
13664
  reorderNativeRecentDom();
13665
+ refreshRecentViews();
13547
13666
  };
13548
13667
 
13549
13668
  const reorderNativeRecentDom = () => {
@@ -13569,6 +13688,18 @@
13569
13688
  const saveRecent = () => {
13570
13689
  recentGalls = tidyRecent(recentGalls);
13571
13690
  writeJson(STORAGE.recent, recentGalls);
13691
+ refreshRecentViews();
13692
+ };
13693
+
13694
+ let recentViewTimer = null;
13695
+
13696
+ const refreshRecentViews = () => {
13697
+ clearTimeout(recentViewTimer);
13698
+ recentViewTimer = setTimeout(() => {
13699
+ if (typeof initialized !== 'undefined' && initialized) {
13700
+ renderRecentBar();
13701
+ }
13702
+ }, 50);
13572
13703
  };
13573
13704
 
13574
13705
  const recentUrl = item => {
@@ -13814,11 +13945,223 @@
13814
13945
  exportNativeRecent();
13815
13946
  };
13816
13947
 
13948
+ const fixNativeLatelyValue = value => {
13949
+ if (!recentOrder.fixed) {
13950
+ return value;
13951
+ }
13952
+
13953
+ try {
13954
+ const parts = unescape(String(value)).split(',').filter(Boolean);
13955
+ const index = new Map(recentGalls.map((item, i) => [item.id, i]));
13956
+ const seen = new Set();
13957
+ const unique = parts.filter(part => {
13958
+ const id = part.split('|')[0];
13959
+
13960
+ if (!id || seen.has(id)) {
13961
+ return false;
13962
+ }
13963
+
13964
+ seen.add(id);
13965
+ return true;
13966
+ });
13967
+ const known = unique.filter(part => index.has(part.split('|')[0])).sort((a, b) => index.get(a.split('|')[0]) - index.get(b.split('|')[0]));
13968
+ const fresh = unique.filter(part => !index.has(part.split('|')[0]));
13969
+
13970
+ return escape([...known, ...fresh].map(part => `${part},`).join(''));
13971
+ } catch {
13972
+ return value;
13973
+ }
13974
+ };
13975
+
13976
+ if (IS_MOBILE_SITE) {
13977
+ try {
13978
+ const proto = window.Storage && window.Storage.prototype;
13979
+ const original = proto && proto.setItem;
13980
+
13981
+ if (original && !original.__dcLately) {
13982
+ const patched = function (key, value) {
13983
+ if (this === window.localStorage && key === 'm_dcinside_lately') {
13984
+ return original.call(this, key, fixNativeLatelyValue(value));
13985
+ }
13986
+
13987
+ return original.call(this, key, value);
13988
+ };
13989
+
13990
+ patched.__dcLately = true;
13991
+ proto.setItem = patched;
13992
+ }
13993
+ } catch {
13994
+ }
13995
+ }
13996
+
13997
+ const renderRecentStrip = () => {
13998
+ const pop = document.getElementById('lately_pop');
13999
+
14000
+ if (!IS_MOBILE_SITE || !pop || settings.mobileRecentStrip === false) {
14001
+ document.documentElement.classList.remove('dc-recent-strip-on');
14002
+ pop?.querySelector('.dc-recent-strip')?.remove();
14003
+ return;
14004
+ }
14005
+
14006
+ const latelyTab = document.getElementById('lately_gall');
14007
+ const showing = !latelyTab || latelyTab.classList.contains('on');
14008
+ let strip = pop.querySelector('.dc-recent-strip');
14009
+
14010
+ if (!strip) {
14011
+ strip = h('div', 'dc-ui dc-recent-strip');
14012
+ strip.setAttribute('role', 'list');
14013
+ const slider = pop.querySelector('#swiper-lately') || pop.querySelector('.vst-slider');
14014
+
14015
+ (slider ? slider.parentNode : pop).insertBefore(strip, slider || null);
14016
+ }
14017
+
14018
+ document.documentElement.classList.add('dc-recent-strip-on');
14019
+ strip.hidden = !showing;
14020
+ pop.classList.toggle('dc-strip-lately', showing);
14021
+
14022
+ if (!showing) {
14023
+ return;
14024
+ }
14025
+
14026
+ const keepScroll = strip.scrollLeft;
14027
+
14028
+ strip.textContent = '';
14029
+
14030
+ if (!recentGalls.length) {
14031
+ strip.appendChild(h('span', 'dc-rs-empty', '최근 방문한 갤러리가 없습니다'));
14032
+ return;
14033
+ }
14034
+
14035
+ const here = (currentGalleryVisit() || {}).id;
14036
+
14037
+ recentGalls.forEach(item => {
14038
+ const chip = h('a', `dc-rs-chip${item.id === here ? ' here' : ''}`);
14039
+
14040
+ chip.href = recentUrl(item);
14041
+ chip.setAttribute('role', 'listitem');
14042
+ chip.appendChild(h('span', 'dc-rs-name', item.n || item.id));
14043
+
14044
+ if (RECENT_KIND_LABEL[item.k]) {
14045
+ chip.appendChild(h('span', 'dc-rs-kind', item.k === 'M' ? 'ⓜ' : item.k === 'N' ? 'ⓝ' : 'ⓟ'));
14046
+ }
14047
+
14048
+ const del = h('button', 'dc-rs-del', '×');
14049
+
14050
+ del.type = 'button';
14051
+ del.setAttribute('aria-label', `${item.n || item.id} 지우기`);
14052
+ del.addEventListener('click', event => {
14053
+ event.preventDefault();
14054
+ event.stopPropagation();
14055
+ recentGalls = recentGalls.filter(other => other !== item);
14056
+ saveRecent();
14057
+ exportNativeRecent();
14058
+ renderRecentStrip();
14059
+ });
14060
+ chip.appendChild(del);
14061
+ strip.appendChild(chip);
14062
+ });
14063
+
14064
+ if (recentOrder.fixed) {
14065
+ strip.appendChild(h('span', 'dc-rs-pin', '📌 순서 고정'));
14066
+ }
14067
+
14068
+ strip.scrollLeft = keepScroll;
14069
+
14070
+ strip.querySelector('.dc-rs-chip.here')?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' });
14071
+ };
14072
+
14073
+ const renderRecentBar = () => {
14074
+ const nav = IS_MOBILE_SITE ? document.querySelector('nav.nav') : null;
14075
+ let bar = document.getElementById('dc-recent-bar');
14076
+
14077
+ if (!nav || settings.mobileRecentBar === false || !recentGalls.length) {
14078
+ bar?.remove();
14079
+ return;
14080
+ }
14081
+
14082
+ if (!bar) {
14083
+ bar = h('div', 'dc-ui');
14084
+ bar.id = 'dc-recent-bar';
14085
+ bar.setAttribute('aria-label', '최근 방문 갤러리');
14086
+ nav.parentNode.insertBefore(bar, nav);
14087
+ }
14088
+
14089
+ try {
14090
+ const cs = getComputedStyle(nav);
14091
+ const active = nav.querySelector('li.on a') || nav.querySelector('a');
14092
+
14093
+ bar.style.setProperty('--dc-rb-bg', cs.backgroundColor);
14094
+ bar.style.setProperty('--dc-rb-on', active ? getComputedStyle(active).color : '#ffe10a');
14095
+ } catch {
14096
+ }
14097
+
14098
+ const keep = bar.scrollLeft;
14099
+ const here = (currentGalleryVisit() || {}).id;
14100
+
14101
+ bar.textContent = '';
14102
+
14103
+ recentGalls.forEach(item => {
14104
+ const link = h('a', `dc-rb-item${item.id === here ? ' on' : ''}`, item.n || item.id);
14105
+
14106
+ link.href = recentUrl(item);
14107
+
14108
+ if (item.k !== 'G') {
14109
+ link.appendChild(h('span', 'dc-rb-kind', item.k === 'M' ? 'ⓜ' : item.k === 'N' ? 'ⓝ' : 'ⓟ'));
14110
+ }
14111
+
14112
+ bar.appendChild(link);
14113
+ });
14114
+
14115
+ bar.scrollLeft = keep;
14116
+
14117
+ const on = bar.querySelector('.dc-rb-item.on');
14118
+
14119
+ if (on && !keep) {
14120
+ const b = bar.getBoundingClientRect();
14121
+ const r = on.getBoundingClientRect();
14122
+
14123
+ if (r.right > b.right || r.left < b.left) {
14124
+ bar.scrollLeft += r.left - b.left - 12;
14125
+ }
14126
+ }
14127
+ };
14128
+
14129
+ const watchRecentStrip = () => {
14130
+ const pop = document.getElementById('lately_pop');
14131
+
14132
+ if (!IS_MOBILE_SITE || !pop) {
14133
+ return;
14134
+ }
14135
+
14136
+ let timer = null;
14137
+ const refresh = () => {
14138
+ clearTimeout(timer);
14139
+ timer = setTimeout(() => {
14140
+ if (pop.style.display !== 'none') {
14141
+ importNativeRecent();
14142
+ renderRecentStrip();
14143
+ }
14144
+ }, 30);
14145
+ };
14146
+
14147
+ new MutationObserver(refresh).observe(pop, { attributes: true, attributeFilter: ['style'] });
14148
+
14149
+ const tabs = pop.querySelector('.tab-lst');
14150
+
14151
+ if (tabs) {
14152
+ new MutationObserver(refresh).observe(tabs, { attributes: true, subtree: true, attributeFilter: ['class'] });
14153
+ }
14154
+
14155
+ renderRecentStrip();
14156
+ };
14157
+
13817
14158
  const initRecent = () => {
13818
14159
  importNativeRecent();
13819
14160
  recordGalleryVisit();
13820
14161
  exportNativeRecent();
13821
14162
  watchNativeRecentDom();
14163
+ watchRecentStrip();
14164
+ renderRecentBar();
13822
14165
 
13823
14166
  const follow = () => {
13824
14167
  if (document.visibilityState === 'visible' && (importNativeRecent() || recentOrder.fixed)) {
@@ -13854,6 +14197,7 @@
13854
14197
  writeJson(STORAGE.recent, recentGalls);
13855
14198
  exportNativeRecent();
13856
14199
  reorderNativeRecentDom();
14200
+ refreshRecentViews();
13857
14201
 
13858
14202
  return true;
13859
14203
  };
@@ -13897,6 +14241,7 @@
13897
14241
  recentGalls = next;
13898
14242
  writeJson(STORAGE.recent, recentGalls);
13899
14243
  exportNativeRecent();
14244
+ refreshRecentViews();
13900
14245
 
13901
14246
  return true;
13902
14247
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kangsicj/dcinsideenhancer",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "DCInside filter & enhancer userscript for mobile and PC (디시인사이드 차단·편의 유저스크립트)",
5
5
  "license": "MIT",
6
6
  "author": "kangsicj",