@kangsicj/dcinsideenhancer 1.3.6 → 1.3.8
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 +4 -3
- package/README.md +4 -3
- package/dist/DCInsideEnhancer.meta.js +1 -1
- package/dist/DCInsideEnhancer.user.js +1441 -43
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// @name DCInside Enhancer
|
|
3
3
|
// @name:ko 디시 인핸서
|
|
4
4
|
// @namespace https://github.com/kangsicj/DCInsideEnhancer
|
|
5
|
-
// @version 1.3.
|
|
5
|
+
// @version 1.3.8
|
|
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 (no account, no tracking).
|
|
7
7
|
// @description:ko 디시인사이드 차단·편의 기능 (모바일·PC) - 유저·단어(제목/본문)·말머리·공지 차단, 같은/비슷한 이미지·움짤 모든 글 차단, 디시콘 제거, 차단 미디어 데이터 절약, 목록 자동 보충·무한 스크롤·뒤로 가기 위치 복원, 스크랩, 이미지 확대뷰 개선, 야간모드 자동 연동, 암호화 기기 간 동기화 (계정 없음·추적 없음)
|
|
8
8
|
// @author kangsicj
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
window.__dcInsideEnhancer = true;
|
|
38
38
|
window.__dcRefinery = true;
|
|
39
39
|
|
|
40
|
-
const SCRIPT_VERSION = '1.3.
|
|
40
|
+
const SCRIPT_VERSION = '1.3.8';
|
|
41
41
|
const PROJECT_URL = 'https://github.com/kangsicj/DCInsideEnhancer';
|
|
42
42
|
const DONATE_URL = 'https://fairy.hada.io/@kangsicj';
|
|
43
43
|
const DONATE_URL_BMC = 'https://buymeacoffee.com/kangsicj';
|
|
@@ -74,7 +74,10 @@
|
|
|
74
74
|
searchHistory: 'dc_search_history',
|
|
75
75
|
blockHits: 'dc_block_hits',
|
|
76
76
|
alarmSeen: 'dc_alarm_marks',
|
|
77
|
-
seenVersion: 'dc_seen_version'
|
|
77
|
+
seenVersion: 'dc_seen_version',
|
|
78
|
+
memos: 'dc_user_memos',
|
|
79
|
+
watch: 'dc_watch',
|
|
80
|
+
liveSeen: 'dc_live_seen'
|
|
78
81
|
};
|
|
79
82
|
|
|
80
83
|
const OBSOLETE_STORAGE = ['dc_media_hash_cache', 'dc_media_host_caps'];
|
|
@@ -194,6 +197,7 @@
|
|
|
194
197
|
hoverPreview: true,
|
|
195
198
|
previewHover: false,
|
|
196
199
|
alarmSystemNotify: false,
|
|
200
|
+
watchAlerts: true,
|
|
197
201
|
pcFitWidth: true,
|
|
198
202
|
autoHideScrollbar: true,
|
|
199
203
|
scrapIntercept: true,
|
|
@@ -203,6 +207,8 @@
|
|
|
203
207
|
autoFillList: true,
|
|
204
208
|
infiniteScroll: true,
|
|
205
209
|
streamOnPost: true,
|
|
210
|
+
liveRefresh: true,
|
|
211
|
+
liveRefreshSec: 60,
|
|
206
212
|
showScrollTop: true,
|
|
207
213
|
keepBlockLog: true,
|
|
208
214
|
logRetentionDays: 30,
|
|
@@ -786,7 +792,9 @@
|
|
|
786
792
|
align-items: center;
|
|
787
793
|
justify-content: flex-end;
|
|
788
794
|
gap: 8px;
|
|
795
|
+
box-sizing: border-box;
|
|
789
796
|
width: 100%;
|
|
797
|
+
max-width: 100%;
|
|
790
798
|
margin: 3px 0 10px;
|
|
791
799
|
font-size: 11px;
|
|
792
800
|
line-height: 1.4;
|
|
@@ -802,6 +810,12 @@
|
|
|
802
810
|
background: var(--dc-ghost-bg);
|
|
803
811
|
}
|
|
804
812
|
|
|
813
|
+
.dc-media-tools.dc-media-tools-blocked:not([style*="width"]),
|
|
814
|
+
.dc-media-tools.dc-media-tools-deferred:not([style*="width"]) {
|
|
815
|
+
width: fit-content;
|
|
816
|
+
min-width: 240px;
|
|
817
|
+
}
|
|
818
|
+
|
|
805
819
|
.dc-media-tools-label {
|
|
806
820
|
color: var(--dc-text);
|
|
807
821
|
opacity: .7;
|
|
@@ -1021,6 +1035,11 @@
|
|
|
1021
1035
|
#dc-custom-modal .dc-modal-tab .dc-tab-ico { flex: 0 0 auto; font-size: 13px; line-height: 1; }
|
|
1022
1036
|
#dc-custom-modal .dc-modal-tab .dc-tab-txt { min-width: 0; overflow: hidden; text-overflow: ellipsis; line-height: 1.2; }
|
|
1023
1037
|
|
|
1038
|
+
@media (max-width: 520px) {
|
|
1039
|
+
#dc-custom-modal .dc-modal-tab { flex-direction: column; gap: 2px; height: 44px; font-size: 11px; letter-spacing: -.5px; }
|
|
1040
|
+
#dc-custom-modal .dc-modal-tab .dc-tab-ico { font-size: 15px; }
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1024
1043
|
#dc-custom-modal .dc-section-title {
|
|
1025
1044
|
display: flex;
|
|
1026
1045
|
align-items: center;
|
|
@@ -2937,6 +2956,160 @@
|
|
|
2937
2956
|
animation: none !important;
|
|
2938
2957
|
}
|
|
2939
2958
|
}
|
|
2959
|
+
|
|
2960
|
+
table.gall_list .gall_writer .addbox { max-width: none !important; flex-wrap: wrap; }
|
|
2961
|
+
table.gall_list .gall_writer .nickname,
|
|
2962
|
+
table.gall_list .gall_writer .addbox .nickname {
|
|
2963
|
+
max-width: 100% !important;
|
|
2964
|
+
white-space: normal !important;
|
|
2965
|
+
overflow: visible !important;
|
|
2966
|
+
text-overflow: clip !important;
|
|
2967
|
+
word-break: break-all;
|
|
2968
|
+
flex-shrink: 1 !important;
|
|
2969
|
+
}
|
|
2970
|
+
table.gall_list .gall_writer .nickname em { line-height: 15px !important; }
|
|
2971
|
+
|
|
2972
|
+
.dc-memo-chip {
|
|
2973
|
+
display: inline-block;
|
|
2974
|
+
max-width: 12em;
|
|
2975
|
+
margin-left: 4px;
|
|
2976
|
+
padding: 0 5px;
|
|
2977
|
+
border-radius: 6px;
|
|
2978
|
+
background: var(--dc-accent-soft);
|
|
2979
|
+
color: var(--dc-accent-ink);
|
|
2980
|
+
font-size: 11px;
|
|
2981
|
+
line-height: 16px;
|
|
2982
|
+
vertical-align: middle;
|
|
2983
|
+
white-space: nowrap;
|
|
2984
|
+
overflow: hidden;
|
|
2985
|
+
text-overflow: ellipsis;
|
|
2986
|
+
font-weight: 500;
|
|
2987
|
+
}
|
|
2988
|
+
.ginfo li .dc-memo-chip { float: none; }
|
|
2989
|
+
.dc-watch-user::before {
|
|
2990
|
+
content: "⭐";
|
|
2991
|
+
margin-right: 2px;
|
|
2992
|
+
font-size: .85em;
|
|
2993
|
+
}
|
|
2994
|
+
.dc-memo-dialog {
|
|
2995
|
+
position: fixed;
|
|
2996
|
+
inset: 0;
|
|
2997
|
+
z-index: 2147483200;
|
|
2998
|
+
display: flex;
|
|
2999
|
+
align-items: center;
|
|
3000
|
+
justify-content: center;
|
|
3001
|
+
padding: 16px;
|
|
3002
|
+
background: var(--dc-overlay);
|
|
3003
|
+
}
|
|
3004
|
+
.dc-memo-box {
|
|
3005
|
+
width: min(420px, 100%);
|
|
3006
|
+
padding: 16px;
|
|
3007
|
+
border-radius: 14px;
|
|
3008
|
+
background: var(--dc-surface);
|
|
3009
|
+
color: var(--dc-text);
|
|
3010
|
+
box-shadow: var(--dc-shadow);
|
|
3011
|
+
font-size: 14px;
|
|
3012
|
+
}
|
|
3013
|
+
.dc-memo-head { display: flex; gap: 8px; align-items: baseline; margin-bottom: 10px; min-width: 0; }
|
|
3014
|
+
.dc-memo-head span { color: var(--dc-text-dim); font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
3015
|
+
.dc-memo-box textarea {
|
|
3016
|
+
box-sizing: border-box;
|
|
3017
|
+
width: 100%;
|
|
3018
|
+
min-height: 96px;
|
|
3019
|
+
padding: 10px;
|
|
3020
|
+
border: 1px solid var(--dc-border);
|
|
3021
|
+
border-radius: 10px;
|
|
3022
|
+
background: var(--dc-surface-2);
|
|
3023
|
+
color: var(--dc-text);
|
|
3024
|
+
font: inherit;
|
|
3025
|
+
font-size: 16px;
|
|
3026
|
+
resize: vertical;
|
|
3027
|
+
}
|
|
3028
|
+
.dc-memo-sub { margin-top: 6px; color: var(--dc-text-dim); font-size: 12px; }
|
|
3029
|
+
.dc-memo-actions { display: flex; gap: 8px; align-items: center; margin-top: 12px; }
|
|
3030
|
+
.dc-memo-actions span { flex: 1; }
|
|
3031
|
+
.dc-memo-actions button {
|
|
3032
|
+
padding: 8px 14px;
|
|
3033
|
+
border: 1px solid var(--dc-border);
|
|
3034
|
+
border-radius: 10px;
|
|
3035
|
+
background: var(--dc-surface);
|
|
3036
|
+
color: var(--dc-text);
|
|
3037
|
+
font-size: 14px;
|
|
3038
|
+
cursor: pointer;
|
|
3039
|
+
}
|
|
3040
|
+
.dc-memo-actions button.primary { border-color: var(--dc-accent); background: var(--dc-accent); color: var(--dc-on-accent); }
|
|
3041
|
+
.dc-memo-actions button.danger { border-color: transparent; background: var(--dc-danger-soft); color: var(--dc-danger-ink); }
|
|
3042
|
+
.dc-block-popover .dc-pop-extra { display: flex; gap: 4px; margin-top: 6px; }
|
|
3043
|
+
.dc-block-popover .dc-pop-mini {
|
|
3044
|
+
flex: 1;
|
|
3045
|
+
padding: 6px 4px;
|
|
3046
|
+
border: 1px solid var(--dc-border);
|
|
3047
|
+
border-radius: 8px;
|
|
3048
|
+
background: transparent;
|
|
3049
|
+
color: var(--dc-text);
|
|
3050
|
+
font-size: 12px;
|
|
3051
|
+
white-space: nowrap;
|
|
3052
|
+
cursor: pointer;
|
|
3053
|
+
}
|
|
3054
|
+
.dc-block-popover .dc-pop-mini:hover { background: var(--dc-accent-soft); }
|
|
3055
|
+
.dc-block-popover .dc-pop-mini.on { border-color: var(--dc-accent); color: var(--dc-accent-ink); }
|
|
3056
|
+
.dc-author-bar {
|
|
3057
|
+
display: flex;
|
|
3058
|
+
gap: 8px;
|
|
3059
|
+
align-items: center;
|
|
3060
|
+
justify-content: space-between;
|
|
3061
|
+
margin: 6px 0;
|
|
3062
|
+
padding: 8px 12px;
|
|
3063
|
+
border-radius: 10px;
|
|
3064
|
+
background: var(--dc-accent-soft);
|
|
3065
|
+
color: var(--dc-text);
|
|
3066
|
+
font-size: 13px;
|
|
3067
|
+
line-height: 1.4;
|
|
3068
|
+
}
|
|
3069
|
+
.dc-author-bar button {
|
|
3070
|
+
flex: none;
|
|
3071
|
+
padding: 4px 10px;
|
|
3072
|
+
border: 1px solid var(--dc-border);
|
|
3073
|
+
border-radius: 8px;
|
|
3074
|
+
background: var(--dc-surface);
|
|
3075
|
+
color: var(--dc-accent-ink);
|
|
3076
|
+
font-size: 12px;
|
|
3077
|
+
cursor: pointer;
|
|
3078
|
+
}
|
|
3079
|
+
.dc-author-other { display: none !important; }
|
|
3080
|
+
html.dc-peek .dc-author-other { display: revert !important; opacity: .45; }
|
|
3081
|
+
.dc-live-pill {
|
|
3082
|
+
position: fixed;
|
|
3083
|
+
top: calc(env(safe-area-inset-top, 0px) + 64px);
|
|
3084
|
+
left: 50%;
|
|
3085
|
+
z-index: 2147483000;
|
|
3086
|
+
padding: 8px 16px;
|
|
3087
|
+
border: none;
|
|
3088
|
+
border-radius: 999px;
|
|
3089
|
+
background: var(--dc-accent);
|
|
3090
|
+
color: var(--dc-on-accent);
|
|
3091
|
+
box-shadow: var(--dc-shadow-sm);
|
|
3092
|
+
font-size: 13px;
|
|
3093
|
+
font-weight: 600;
|
|
3094
|
+
opacity: 0;
|
|
3095
|
+
transform: translate(-50%, -12px);
|
|
3096
|
+
pointer-events: none;
|
|
3097
|
+
transition: opacity .2s ease, transform .2s ease;
|
|
3098
|
+
cursor: pointer;
|
|
3099
|
+
}
|
|
3100
|
+
.dc-live-pill.show { opacity: 1; transform: translate(-50%, 0); pointer-events: auto; }
|
|
3101
|
+
.dc-live-new { animation: dc-live-flash 3.5s ease-out; }
|
|
3102
|
+
.dc-live-new > td { animation: dc-live-flash 3.5s ease-out; }
|
|
3103
|
+
@keyframes dc-live-flash {
|
|
3104
|
+
0%, 40% { background-color: rgba(91,110,214,.16); }
|
|
3105
|
+
100% { background-color: transparent; }
|
|
3106
|
+
}
|
|
3107
|
+
.dc-live-watch .subjectin,
|
|
3108
|
+
.dc-live-watch .gall_tit a:first-child { font-weight: 700; }
|
|
3109
|
+
@media (prefers-reduced-motion: reduce) {
|
|
3110
|
+
.dc-live-pill { transition: none; }
|
|
3111
|
+
.dc-live-new, .dc-live-new > td { animation: none; }
|
|
3112
|
+
}
|
|
2940
3113
|
`;
|
|
2941
3114
|
|
|
2942
3115
|
let styleEl = null;
|
|
@@ -3542,7 +3715,7 @@
|
|
|
3542
3715
|
|
|
3543
3716
|
let netQueue = Promise.resolve();
|
|
3544
3717
|
|
|
3545
|
-
const siteFetchText = (url, options = {}, { background = false, auto = false } = {}) => {
|
|
3718
|
+
const siteFetchText = (url, options = {}, { background = false, auto = false, probe = false } = {}) => {
|
|
3546
3719
|
const run = async () => {
|
|
3547
3720
|
let state = readNetState();
|
|
3548
3721
|
|
|
@@ -3623,6 +3796,14 @@
|
|
|
3623
3796
|
|
|
3624
3797
|
let { response, text } = await fetchOnce('');
|
|
3625
3798
|
|
|
3799
|
+
if (probe) {
|
|
3800
|
+
if (!response.ok) {
|
|
3801
|
+
throw new Error(`HTTP ${response.status}`);
|
|
3802
|
+
}
|
|
3803
|
+
|
|
3804
|
+
return text;
|
|
3805
|
+
}
|
|
3806
|
+
|
|
3626
3807
|
if (looksBlockedResponse(response, text)) {
|
|
3627
3808
|
if (unreliableWindow(startedAt) || truncatedResponse(response, text)) {
|
|
3628
3809
|
throw new NetTransientError('unreliable');
|
|
@@ -5697,6 +5878,76 @@
|
|
|
5697
5878
|
const isDeferActive = () =>
|
|
5698
5879
|
settings.deferHeavyMedia === true || (settings.autoSaveData !== false && connectionSaveData());
|
|
5699
5880
|
|
|
5881
|
+
const mediaBoxes = new WeakMap();
|
|
5882
|
+
|
|
5883
|
+
const fitToolsToMedia = (mediaEl, tools) => {
|
|
5884
|
+
const parent = tools.parentElement;
|
|
5885
|
+
|
|
5886
|
+
let box = mediaBoxes.get(mediaEl) || tools._dcBox;
|
|
5887
|
+
|
|
5888
|
+
if (mediaBoxes.has(mediaEl)) {
|
|
5889
|
+
tools._dcBox = box;
|
|
5890
|
+
}
|
|
5891
|
+
|
|
5892
|
+
if (!box && mediaEl.tagName === 'IMG') {
|
|
5893
|
+
const width = Number(mediaEl.getAttribute('width')) || mediaEl.naturalWidth || 0;
|
|
5894
|
+
|
|
5895
|
+
box = width > 40 ? { width, left: null } : null;
|
|
5896
|
+
}
|
|
5897
|
+
|
|
5898
|
+
if (!box || !parent) {
|
|
5899
|
+
tools.style.removeProperty('width');
|
|
5900
|
+
tools.style.removeProperty('margin-left');
|
|
5901
|
+
return;
|
|
5902
|
+
}
|
|
5903
|
+
|
|
5904
|
+
const parentRect = parent.getBoundingClientRect();
|
|
5905
|
+
const room = parent.clientWidth || parentRect.width;
|
|
5906
|
+
const width = Math.min(box.width, room || box.width);
|
|
5907
|
+
let left = box.left === null ? 0 : box.left - parentRect.left - (parseFloat(getComputedStyle(parent).paddingLeft) || 0);
|
|
5908
|
+
|
|
5909
|
+
if (!(left > 0) || left + width > room + 1) {
|
|
5910
|
+
left = 0;
|
|
5911
|
+
}
|
|
5912
|
+
|
|
5913
|
+
tools.style.setProperty('width', `${Math.round(width)}px`);
|
|
5914
|
+
tools.style.setProperty('margin-left', `${Math.round(left)}px`);
|
|
5915
|
+
};
|
|
5916
|
+
|
|
5917
|
+
const refitMediaTools = mediaEl => {
|
|
5918
|
+
const tools = mediaTools.get(mediaEl);
|
|
5919
|
+
const host = mediaHosts.get(mediaEl) || mediaEl;
|
|
5920
|
+
|
|
5921
|
+
if (!tools || !tools.isConnected) {
|
|
5922
|
+
return;
|
|
5923
|
+
}
|
|
5924
|
+
|
|
5925
|
+
if (!host.classList.contains('dc-media-hidden')) {
|
|
5926
|
+
const shown = mediaEl.getBoundingClientRect();
|
|
5927
|
+
|
|
5928
|
+
if (shown.width > 40) {
|
|
5929
|
+
mediaBoxes.set(mediaEl, { width: shown.width, left: shown.left });
|
|
5930
|
+
}
|
|
5931
|
+
}
|
|
5932
|
+
|
|
5933
|
+
fitToolsToMedia(mediaEl, tools);
|
|
5934
|
+
};
|
|
5935
|
+
|
|
5936
|
+
let refitTimer = null;
|
|
5937
|
+
|
|
5938
|
+
window.addEventListener('resize', () => {
|
|
5939
|
+
clearTimeout(refitTimer);
|
|
5940
|
+
refitTimer = setTimeout(() => {
|
|
5941
|
+
document.querySelectorAll('.dc-media-tools').forEach(tools => {
|
|
5942
|
+
const mediaEl = tools.previousElementSibling && tools.previousElementSibling.matches('img, video') ? tools.previousElementSibling : null;
|
|
5943
|
+
|
|
5944
|
+
if (mediaEl) {
|
|
5945
|
+
refitMediaTools(mediaEl);
|
|
5946
|
+
}
|
|
5947
|
+
});
|
|
5948
|
+
}, 200);
|
|
5949
|
+
}, { passive: true });
|
|
5950
|
+
|
|
5700
5951
|
const readToolIds = tools => (tools.dataset.dcMediaIds || '').split(',').filter(Boolean);
|
|
5701
5952
|
|
|
5702
5953
|
const createTools = () => {
|
|
@@ -5796,6 +6047,12 @@
|
|
|
5796
6047
|
mediaEl.classList.remove('dc-media-hidden');
|
|
5797
6048
|
}
|
|
5798
6049
|
|
|
6050
|
+
const shown = mediaEl.getBoundingClientRect();
|
|
6051
|
+
|
|
6052
|
+
if (shown.width > 40 && !blocked && !deferred && !host.classList.contains('dc-media-hidden')) {
|
|
6053
|
+
mediaBoxes.set(mediaEl, { width: shown.width, left: shown.left });
|
|
6054
|
+
}
|
|
6055
|
+
|
|
5799
6056
|
host.classList.toggle('dc-media-hidden', blocked || deferred);
|
|
5800
6057
|
|
|
5801
6058
|
const tools = existing && existing.isConnected ? existing : createTools();
|
|
@@ -5824,6 +6081,12 @@
|
|
|
5824
6081
|
tools.querySelector('.dc-media-btn-load').hidden = !deferred;
|
|
5825
6082
|
|
|
5826
6083
|
mediaSignature.set(mediaEl, signature);
|
|
6084
|
+
fitToolsToMedia(mediaEl, tools);
|
|
6085
|
+
|
|
6086
|
+
if (!mediaBoxes.has(mediaEl) && !blocked && !deferred && !mediaEl.dataset.dcFitWatch) {
|
|
6087
|
+
mediaEl.dataset.dcFitWatch = '1';
|
|
6088
|
+
mediaEl.addEventListener(mediaEl.tagName === 'VIDEO' ? 'loadedmetadata' : 'load', () => refitMediaTools(mediaEl), { once: true });
|
|
6089
|
+
}
|
|
5827
6090
|
|
|
5828
6091
|
return tools;
|
|
5829
6092
|
};
|
|
@@ -5872,12 +6135,36 @@
|
|
|
5872
6135
|
});
|
|
5873
6136
|
};
|
|
5874
6137
|
|
|
6138
|
+
const zoomSourceUrl = el => {
|
|
6139
|
+
const rel = el.getAttribute('data-rel');
|
|
6140
|
+
let url = rel && rel !== 'null' ? rel : el.getAttribute('data-original') || el.getAttribute('ori-data');
|
|
6141
|
+
|
|
6142
|
+
if (el.hasAttribute('data-gif')) {
|
|
6143
|
+
url = el.getAttribute('data-gif');
|
|
6144
|
+
}
|
|
6145
|
+
|
|
6146
|
+
return url || '';
|
|
6147
|
+
};
|
|
6148
|
+
|
|
6149
|
+
const findZoomSource = (mediaEl, slide, popup, zoomSources) => {
|
|
6150
|
+
const slideSrc = mediaEl.getAttribute('src') || '';
|
|
6151
|
+
const bySrc = slideSrc && zoomSources.find(el => zoomSourceUrl(el) === slideSrc);
|
|
6152
|
+
|
|
6153
|
+
if (bySrc) {
|
|
6154
|
+
return bySrc;
|
|
6155
|
+
}
|
|
6156
|
+
|
|
6157
|
+
const index = slide && popup ? [...popup.querySelectorAll('.swiper-slide')].indexOf(slide) : -1;
|
|
6158
|
+
const listed = zoomSources.filter(el => zoomSourceUrl(el));
|
|
6159
|
+
|
|
6160
|
+
return index >= 0 ? listed[index] || null : null;
|
|
6161
|
+
};
|
|
6162
|
+
|
|
5875
6163
|
const applyZoomSlideBlock = (element, zoomSources) => {
|
|
5876
6164
|
const mediaEl = getMediaElement(element);
|
|
5877
6165
|
const slide = mediaEl.closest('.swiper-slide');
|
|
5878
6166
|
const popup = mediaEl.closest(ZOOM_POPUP_SELECTOR);
|
|
5879
|
-
const
|
|
5880
|
-
const source = index >= 0 ? zoomSources[index] : null;
|
|
6167
|
+
const source = findZoomSource(mediaEl, slide, popup, zoomSources);
|
|
5881
6168
|
const ids = [...new Set([...resolveMediaIds(mediaEl), ...(source ? resolveMediaIds(source) : [])])];
|
|
5882
6169
|
|
|
5883
6170
|
if (!ids.length) {
|
|
@@ -6137,19 +6424,29 @@
|
|
|
6137
6424
|
}
|
|
6138
6425
|
|
|
6139
6426
|
let counts = null;
|
|
6427
|
+
let answered = false;
|
|
6140
6428
|
|
|
6141
6429
|
const $ = window.jQuery;
|
|
6142
6430
|
|
|
6143
6431
|
if ($ && $.ajax) {
|
|
6144
6432
|
counts = await new Promise(resolve => {
|
|
6145
6433
|
$.ajax({ url: '/ajax/userCnt', type: 'post', data: { user: code }, timeout: 6000 })
|
|
6146
|
-
.done(res =>
|
|
6434
|
+
.done(res => {
|
|
6435
|
+
answered = true;
|
|
6436
|
+
resolve(res && res.board ? { post: String(res.board), reply: String(res.comment || '0') } : null);
|
|
6437
|
+
})
|
|
6147
6438
|
.fail(() => resolve(null));
|
|
6148
6439
|
});
|
|
6149
6440
|
}
|
|
6150
6441
|
|
|
6442
|
+
if (answered) {
|
|
6443
|
+
gallogCountCache.set(code, counts);
|
|
6444
|
+
|
|
6445
|
+
return counts;
|
|
6446
|
+
}
|
|
6447
|
+
|
|
6151
6448
|
if (!counts) {
|
|
6152
|
-
const html = await siteFetchText(`https://m.dcinside.com/gallog/${encodeURIComponent(code)}`, { credentials: 'same-origin' });
|
|
6449
|
+
const html = await siteFetchText(`https://m.dcinside.com/gallog/${encodeURIComponent(code)}`, { credentials: 'same-origin' }, { probe: true });
|
|
6153
6450
|
const text = html.replace(/<[^>]+>/g, ' ');
|
|
6154
6451
|
const post = (text.match(/게시물\s*\(\s*([\d,]+)\s*\)/) || [])[1] || '';
|
|
6155
6452
|
const reply = (text.match(/댓글\s*\(\s*([\d,]+)\s*\)/) || [])[1] || '';
|
|
@@ -6183,10 +6480,11 @@
|
|
|
6183
6480
|
const ip = (keys.find(k => k.type === 'ip') || {}).value || '';
|
|
6184
6481
|
const target = code || ip;
|
|
6185
6482
|
const gallog = code ? `/gallog/${encodeURIComponent(code)}` : '';
|
|
6186
|
-
const search = mobileSearchUrl(nick);
|
|
6483
|
+
const search = authorSearchUrl(keys) || mobileSearchUrl(nick);
|
|
6187
6484
|
const me = (document.getElementById('user_id') || {}).value || '';
|
|
6188
6485
|
const isMe = !!code && code === me;
|
|
6189
|
-
const
|
|
6486
|
+
const memo = memoForKeys(keys);
|
|
6487
|
+
const watched = isWatchedKeys(keys);
|
|
6190
6488
|
const blockLabel = (type, value, label) => `${blockedUsers.includes(`${type}::${value}`) ? '차단 해제' : '차단'}(${label})`;
|
|
6191
6489
|
const row = (attrs, label) => `<li><a ${attrs}>${escapeHtml(label)}</a></li>`;
|
|
6192
6490
|
const menu = document.createElement('div');
|
|
@@ -6209,7 +6507,8 @@
|
|
|
6209
6507
|
'<ul class="lst">' +
|
|
6210
6508
|
(code ? row(`href="${escapeHtml(gallog)}"`, '갤로그') : '') +
|
|
6211
6509
|
(search ? row(`href="${escapeHtml(search)}"`, '작성글 검색') : '') +
|
|
6212
|
-
(!isMe
|
|
6510
|
+
(!isMe ? row('href="#" data-act="memo"', memo ? `📝 메모 · ${memo.text.length > 14 ? `${memo.text.slice(0, 14)}…` : memo.text}` : '📝 메모 남기기') : '') +
|
|
6511
|
+
(!isMe ? row('href="#" data-act="watch"', watched ? '⭐ 관심 사용자 해제' : '☆ 관심 사용자 (새 글 알림)') : '') +
|
|
6213
6512
|
(!isMe && nick ? row(`href="#" data-key="${escapeHtml(`nick::${nick}`)}"`, blockLabel('nick', nick, '닉네임')) : '') +
|
|
6214
6513
|
(!isMe && code ? row(`href="#" data-key="${escapeHtml(`id::${code}`)}"`, blockLabel('id', code, '식별 코드')) : '') +
|
|
6215
6514
|
(!isMe && !code && ip ? row(`href="#" data-key="${escapeHtml(`ip::${ip}`)}"`, blockLabel('ip', ip, 'IP')) : '') +
|
|
@@ -6227,7 +6526,14 @@
|
|
|
6227
6526
|
if (link.dataset.act === 'memo') {
|
|
6228
6527
|
event.preventDefault();
|
|
6229
6528
|
closePopover();
|
|
6230
|
-
|
|
6529
|
+
openMemoEditor(keys, nick || target);
|
|
6530
|
+
return;
|
|
6531
|
+
}
|
|
6532
|
+
|
|
6533
|
+
if (link.dataset.act === 'watch') {
|
|
6534
|
+
event.preventDefault();
|
|
6535
|
+
closePopover();
|
|
6536
|
+
toggleWatchUser(keys);
|
|
6231
6537
|
return;
|
|
6232
6538
|
}
|
|
6233
6539
|
|
|
@@ -6304,9 +6610,36 @@
|
|
|
6304
6610
|
);
|
|
6305
6611
|
})
|
|
6306
6612
|
.join('') +
|
|
6613
|
+
'<div class="dc-pop-extra">' +
|
|
6614
|
+
`<button type="button" class="dc-pop-mini" data-act="memo" title="${memoForKeys(keys) ? '메모 고치기' : '메모 남기기'}">📝 메모</button>` +
|
|
6615
|
+
`<button type="button" class="dc-pop-mini${isWatchedKeys(keys) ? ' on' : ''}" data-act="watch" title="${isWatchedKeys(keys) ? '관심 사용자 해제' : '관심 사용자 등록 (새 글 알림)'}">${isWatchedKeys(keys) ? '⭐ 관심' : '☆ 관심'}</button>` +
|
|
6616
|
+
(authorSearchUrl(keys) ? '<button type="button" class="dc-pop-mini" data-act="search">🔍 작성글</button>' : '') +
|
|
6617
|
+
'</div>' +
|
|
6307
6618
|
|
|
6308
6619
|
'<button type="button" class="dc-pop-site">⋯ 사이트 메뉴 (갤로그·작성글)</button>';
|
|
6309
6620
|
|
|
6621
|
+
popover.querySelector('.dc-pop-extra').addEventListener('click', event => {
|
|
6622
|
+
const act = event.target instanceof Element ? event.target.closest('[data-act]')?.dataset.act : '';
|
|
6623
|
+
|
|
6624
|
+
if (!act) {
|
|
6625
|
+
return;
|
|
6626
|
+
}
|
|
6627
|
+
|
|
6628
|
+
event.preventDefault();
|
|
6629
|
+
event.stopPropagation();
|
|
6630
|
+
closePopover();
|
|
6631
|
+
|
|
6632
|
+
const nickKey = keys.find(k => k.type === 'nick');
|
|
6633
|
+
|
|
6634
|
+
if (act === 'memo') {
|
|
6635
|
+
openMemoEditor(keys, nickKey ? nickKey.value : '');
|
|
6636
|
+
} else if (act === 'watch') {
|
|
6637
|
+
toggleWatchUser(keys);
|
|
6638
|
+
} else if (act === 'search') {
|
|
6639
|
+
location.href = authorSearchUrl(keys);
|
|
6640
|
+
}
|
|
6641
|
+
});
|
|
6642
|
+
|
|
6310
6643
|
popover.querySelector('.dc-pop-site').addEventListener('click', event => {
|
|
6311
6644
|
event.preventDefault();
|
|
6312
6645
|
event.stopPropagation();
|
|
@@ -7112,6 +7445,25 @@
|
|
|
7112
7445
|
hideEmptyReplyGroups(collapse);
|
|
7113
7446
|
});
|
|
7114
7447
|
|
|
7448
|
+
const restoreFullNick = (nickElement, fullNick) => {
|
|
7449
|
+
if (!fullNick || !nickElement || nickElement.dataset.dcFullNick) {
|
|
7450
|
+
return;
|
|
7451
|
+
}
|
|
7452
|
+
|
|
7453
|
+
const walker = document.createTreeWalker(nickElement, NodeFilter.SHOW_TEXT);
|
|
7454
|
+
|
|
7455
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
7456
|
+
const match = node.nodeValue.match(/^(\s*)(.+?)(\.\.|…)(\s*(?:\([^)]*\))?\s*)$/);
|
|
7457
|
+
|
|
7458
|
+
if (match && match[2].length < fullNick.length && fullNick.startsWith(match[2].trim())) {
|
|
7459
|
+
node.nodeValue = `${match[1]}${fullNick}${match[4]}`;
|
|
7460
|
+
nickElement.dataset.dcFullNick = 'true';
|
|
7461
|
+
nickElement.title = fullNick;
|
|
7462
|
+
break;
|
|
7463
|
+
}
|
|
7464
|
+
}
|
|
7465
|
+
};
|
|
7466
|
+
|
|
7115
7467
|
const processUserNode = (container, nickElement, source) => {
|
|
7116
7468
|
if (!container || !nickElement || container.dataset.dcProcessed) {
|
|
7117
7469
|
return;
|
|
@@ -7133,7 +7485,9 @@
|
|
|
7133
7485
|
|
|
7134
7486
|
const keys = buildBlockKeys(info);
|
|
7135
7487
|
|
|
7488
|
+
restoreFullNick(nickElement, info.nickname);
|
|
7136
7489
|
attachBlockTrigger(nickElement, keys);
|
|
7490
|
+
decorateUserNode(nickElement, keys);
|
|
7137
7491
|
|
|
7138
7492
|
const area = IS_MOBILE_SITE ? nickElement.closest('.ginfo-area') : null;
|
|
7139
7493
|
|
|
@@ -7390,6 +7744,7 @@
|
|
|
7390
7744
|
processMediaElements();
|
|
7391
7745
|
pinReadColors();
|
|
7392
7746
|
applyUserBlockState();
|
|
7747
|
+
applyAuthorFilter();
|
|
7393
7748
|
applyHeadTabVisibility();
|
|
7394
7749
|
bindHeadTabLongPress();
|
|
7395
7750
|
maintainListStream();
|
|
@@ -9842,6 +10197,7 @@
|
|
|
9842
10197
|
];
|
|
9843
10198
|
|
|
9844
10199
|
let blockSection = 'keywords';
|
|
10200
|
+
let userBlockType = 'nick';
|
|
9845
10201
|
let blockFilter = '';
|
|
9846
10202
|
|
|
9847
10203
|
const makeInputRow = (placeholder, buttonLabel, onSubmit, { maxLength = WORD_MAX_LENGTH } = {}) => {
|
|
@@ -10158,39 +10514,107 @@
|
|
|
10158
10514
|
},
|
|
10159
10515
|
|
|
10160
10516
|
users: area => {
|
|
10161
|
-
|
|
10517
|
+
const entries = blockedUsers.map(parseBlockKey);
|
|
10518
|
+
const count = type => entries.filter(entry => entry.type === type).length;
|
|
10519
|
+
|
|
10520
|
+
area.append(
|
|
10521
|
+
makeSectionTitle(`👤 유저 차단 (${blockedUsers.length})`),
|
|
10522
|
+
makeNote('기준마다 따로 모아 봅니다. 닉네임 = 같은 닉네임 모두(유동 포함), 식별코드 = 그 고닉 한 사람, IP = 그 유동 IP 대역. 닉네임을 눌러 차단하면 이곳에 쌓입니다.')
|
|
10523
|
+
);
|
|
10524
|
+
|
|
10525
|
+
const seg = h('div', 'dc-seg dc-word-scope');
|
|
10526
|
+
|
|
10527
|
+
Object.keys(BLOCK_TYPE_LABELS).forEach(type => {
|
|
10528
|
+
const btn = h('button', `dc-seg-btn${userBlockType === type ? ' active' : ''}`);
|
|
10529
|
+
|
|
10530
|
+
btn.type = 'button';
|
|
10531
|
+
btn.setAttribute('aria-pressed', String(userBlockType === type));
|
|
10532
|
+
btn.append(h('span', '', BLOCK_TYPE_LABELS[type]), h('span', 'dc-seg-count', String(count(type))));
|
|
10533
|
+
btn.addEventListener('click', () => {
|
|
10534
|
+
userBlockType = type;
|
|
10535
|
+
blockFilter = '';
|
|
10536
|
+
rerenderModal();
|
|
10537
|
+
});
|
|
10538
|
+
seg.appendChild(btn);
|
|
10539
|
+
});
|
|
10540
|
+
|
|
10541
|
+
area.appendChild(seg);
|
|
10542
|
+
area.appendChild(
|
|
10543
|
+
makeInputRow(
|
|
10544
|
+
userBlockType === 'ip' ? 'IP 앞자리 (예: 118.235)' : userBlockType === 'id' ? '식별코드 (갤로그 주소의 아이디)' : '닉네임',
|
|
10545
|
+
'차단',
|
|
10546
|
+
value => {
|
|
10547
|
+
const values = String(value)
|
|
10548
|
+
.split(/[,\n]/)
|
|
10549
|
+
.map(item => item.trim())
|
|
10550
|
+
.filter(Boolean)
|
|
10551
|
+
.filter(item => userBlockType !== 'ip' || /^\d{1,3}\.\d{1,3}$/.test(item));
|
|
10552
|
+
let added = 0;
|
|
10553
|
+
|
|
10554
|
+
values.forEach(item => {
|
|
10555
|
+
const key = `${userBlockType}::${item}`;
|
|
10556
|
+
|
|
10557
|
+
if (!blockedUsers.includes(key)) {
|
|
10558
|
+
blockedUsers = [...blockedUsers, key];
|
|
10559
|
+
added += 1;
|
|
10560
|
+
}
|
|
10561
|
+
});
|
|
10562
|
+
|
|
10563
|
+
if (added) {
|
|
10564
|
+
saveUsers();
|
|
10565
|
+
refreshAfterBlockChange();
|
|
10566
|
+
} else if (userBlockType === 'ip' && values.length === 0) {
|
|
10567
|
+
showToast('IP 는 앞 두 자리(예: 118.235) 형식으로 넣어 주세요');
|
|
10568
|
+
}
|
|
10569
|
+
|
|
10570
|
+
rerenderModal();
|
|
10571
|
+
},
|
|
10572
|
+
{ maxLength: 400 }
|
|
10573
|
+
)
|
|
10574
|
+
);
|
|
10162
10575
|
|
|
10163
|
-
const
|
|
10576
|
+
const list = entries.filter(entry => entry.type === userBlockType);
|
|
10577
|
+
const filterRow = makeFilterRow(list.length);
|
|
10164
10578
|
|
|
10165
10579
|
if (filterRow) {
|
|
10166
10580
|
area.appendChild(filterRow);
|
|
10167
10581
|
}
|
|
10168
10582
|
|
|
10169
|
-
if (!
|
|
10170
|
-
area.appendChild(makeEmpty(
|
|
10171
|
-
return;
|
|
10583
|
+
if (!list.length) {
|
|
10584
|
+
area.appendChild(makeEmpty(`${BLOCK_TYPE_LABELS[userBlockType]}(으)로 차단한 유저가 없습니다.`));
|
|
10172
10585
|
}
|
|
10173
10586
|
|
|
10174
|
-
|
|
10175
|
-
.map(parseBlockKey)
|
|
10176
|
-
.forEach(entry => {
|
|
10177
|
-
area.appendChild(
|
|
10178
|
-
makeRow({
|
|
10179
|
-
title: entry.value,
|
|
10180
|
-
meta: `${BLOCK_TYPE_LABELS[entry.type]} 기준`,
|
|
10181
|
-
actions: [{ label: '해제', className: 'on', onClick: () => { toggleUserBlockKey(entry.key); rerenderModal(); } }]
|
|
10182
|
-
})
|
|
10183
|
-
);
|
|
10184
|
-
});
|
|
10587
|
+
const memos = readMemos();
|
|
10185
10588
|
|
|
10186
|
-
|
|
10187
|
-
|
|
10188
|
-
|
|
10189
|
-
|
|
10190
|
-
|
|
10191
|
-
|
|
10192
|
-
|
|
10193
|
-
|
|
10589
|
+
list.forEach(entry => {
|
|
10590
|
+
const memo = (memos[entry.key] || {}).t;
|
|
10591
|
+
|
|
10592
|
+
area.appendChild(
|
|
10593
|
+
makeRow({
|
|
10594
|
+
title: entry.value,
|
|
10595
|
+
chip: BLOCK_TYPE_LABELS[entry.type],
|
|
10596
|
+
chipClass: BLOCK_TYPE_CLASS[entry.type],
|
|
10597
|
+
meta: memo ? `📝 ${memo}` : '',
|
|
10598
|
+
actions: [
|
|
10599
|
+
{ label: '메모', onClick: () => openMemoEditor([entry], entry.value) },
|
|
10600
|
+
{ label: '해제', className: 'on', onClick: () => { toggleUserBlockKey(entry.key); rerenderModal(); } }
|
|
10601
|
+
]
|
|
10602
|
+
})
|
|
10603
|
+
);
|
|
10604
|
+
});
|
|
10605
|
+
|
|
10606
|
+
applyListFilter(area);
|
|
10607
|
+
|
|
10608
|
+
if (list.length) {
|
|
10609
|
+
area.appendChild(
|
|
10610
|
+
makeConfirmButton(`${BLOCK_TYPE_LABELS[userBlockType]} 차단 전체 해제`, `${BLOCK_TYPE_LABELS[userBlockType]}(으)로 차단한 유저 ${list.length}명을 모두 해제할까요?`, () => {
|
|
10611
|
+
blockedUsers = blockedUsers.filter(key => parseBlockKey(key).type !== userBlockType);
|
|
10612
|
+
saveUsers();
|
|
10613
|
+
refreshAfterBlockChange();
|
|
10614
|
+
rerenderModal();
|
|
10615
|
+
})
|
|
10616
|
+
);
|
|
10617
|
+
}
|
|
10194
10618
|
},
|
|
10195
10619
|
|
|
10196
10620
|
media: area => {
|
|
@@ -10477,6 +10901,19 @@
|
|
|
10477
10901
|
off: '이 탭을 볼 때 말풍선으로만 알립니다.',
|
|
10478
10902
|
refresh: 'root'
|
|
10479
10903
|
},
|
|
10904
|
+
{
|
|
10905
|
+
group: 'alarm',
|
|
10906
|
+
key: 'watchAlerts',
|
|
10907
|
+
name: '관심 키워드·관심 사용자 새 글 알림',
|
|
10908
|
+
on: '새 글 자동 이어 붙이기로 들어온 글 중 관심 키워드가 제목에 있거나 관심 사용자가 쓴 글이면 알림을 띄웁니다. 관심 목록은 🔔 관심 탭에서 관리합니다.',
|
|
10909
|
+
off: '관심 글도 따로 알리지 않습니다 (목록에는 그대로 붙습니다).',
|
|
10910
|
+
status: on => {
|
|
10911
|
+
const watch = readWatch();
|
|
10912
|
+
|
|
10913
|
+
return on ? `관심 키워드 ${watch.words.length}개 · 관심 사용자 ${watch.users.length}명${settings.liveRefresh === false ? ' · 새 글 자동 이어 붙이기가 꺼져 있어 쉬는 중' : ''}` : null;
|
|
10914
|
+
},
|
|
10915
|
+
refresh: 'none'
|
|
10916
|
+
},
|
|
10480
10917
|
{
|
|
10481
10918
|
group: 'alarm',
|
|
10482
10919
|
key: 'alarmKeepPolling',
|
|
@@ -10555,6 +10992,29 @@
|
|
|
10555
10992
|
: null,
|
|
10556
10993
|
refresh: 'stream'
|
|
10557
10994
|
},
|
|
10995
|
+
{
|
|
10996
|
+
group: 'list',
|
|
10997
|
+
key: 'liveRefresh',
|
|
10998
|
+
name: '새 글 자동으로 이어 붙이기',
|
|
10999
|
+
on: '목록 1페이지를 보는 동안 새 글을 확인해 새로고침 없이 맨 위에 붙입니다. 내려서 읽는 중이면 읽던 자리를 그대로 두고 [↑ 새 글 N개] 만 띄웁니다. 화면을 보고 있을 때만, 10분 넘게 손대지 않으면 쉽니다.',
|
|
11000
|
+
off: '새 글을 보려면 직접 새로고침합니다.',
|
|
11001
|
+
status: on => (on ? liveStatusText() : null),
|
|
11002
|
+
refresh: 'live'
|
|
11003
|
+
},
|
|
11004
|
+
{
|
|
11005
|
+
group: 'list',
|
|
11006
|
+
key: 'liveRefreshSec',
|
|
11007
|
+
parent: 'liveRefresh',
|
|
11008
|
+
name: '새 글 확인 간격',
|
|
11009
|
+
on: '짧을수록 빨리 보이지만 요청이 늘어납니다. 디시 서버에 부담이 되지 않게 30초보다 짧게는 확인하지 않습니다.',
|
|
11010
|
+
choices: [
|
|
11011
|
+
{ value: 30, label: '30초' },
|
|
11012
|
+
{ value: 60, label: '1분' },
|
|
11013
|
+
{ value: 120, label: '2분' },
|
|
11014
|
+
{ value: 300, label: '5분' }
|
|
11015
|
+
],
|
|
11016
|
+
refresh: 'live'
|
|
11017
|
+
},
|
|
10558
11018
|
{
|
|
10559
11019
|
group: 'list',
|
|
10560
11020
|
key: 'hideReadMark',
|
|
@@ -10757,6 +11217,10 @@
|
|
|
10757
11217
|
processElements();
|
|
10758
11218
|
break;
|
|
10759
11219
|
|
|
11220
|
+
case 'live':
|
|
11221
|
+
scheduleLive();
|
|
11222
|
+
break;
|
|
11223
|
+
|
|
10760
11224
|
case 'theme':
|
|
10761
11225
|
coordinateOtherDarkScripts();
|
|
10762
11226
|
|
|
@@ -10785,7 +11249,7 @@
|
|
|
10785
11249
|
|
|
10786
11250
|
const BACKUP_APP = 'dcinside-enhancer';
|
|
10787
11251
|
const BACKUP_APP_OLD = ['dc-refinery', 'dc-optimizer'];
|
|
10788
|
-
const BACKUP_KEYS = ['users', 'images', 'keywords', 'titles', 'heads', 'settings', 'scraps'];
|
|
11252
|
+
const BACKUP_KEYS = ['users', 'images', 'keywords', 'titles', 'heads', 'settings', 'scraps', 'memos', 'watch'];
|
|
10789
11253
|
const USER_KEY_RE = /^(?:nick|ip|id)::.+/;
|
|
10790
11254
|
|
|
10791
11255
|
const exportData = () => ({
|
|
@@ -10799,6 +11263,8 @@
|
|
|
10799
11263
|
keywords: blockedKeywords,
|
|
10800
11264
|
titles: blockedTitles,
|
|
10801
11265
|
heads: hiddenHeadMap,
|
|
11266
|
+
memos: Object.fromEntries(Object.entries(readMemos()).filter(([, item]) => item.t)),
|
|
11267
|
+
watch: readWatch(),
|
|
10802
11268
|
settings
|
|
10803
11269
|
});
|
|
10804
11270
|
|
|
@@ -10856,12 +11322,36 @@
|
|
|
10856
11322
|
|
|
10857
11323
|
const importedScraps = Array.isArray(parsed.scraps) ? uniqueScraps(parsed.scraps.filter(isValidScrap).map(normalizeScrap)) : null;
|
|
10858
11324
|
|
|
11325
|
+
let memos = null;
|
|
11326
|
+
|
|
11327
|
+
if (parsed.memos && typeof parsed.memos === 'object' && !Array.isArray(parsed.memos)) {
|
|
11328
|
+
memos = {};
|
|
11329
|
+
Object.entries(parsed.memos).forEach(([key, item]) => {
|
|
11330
|
+
const text = item && typeof item === 'object' ? String(item.t || '').trim().slice(0, 300) : '';
|
|
11331
|
+
|
|
11332
|
+
if (USER_KEY_RE.test(key) && text) {
|
|
11333
|
+
memos[key] = { t: text, at: Number(item.at) || Date.now() };
|
|
11334
|
+
}
|
|
11335
|
+
});
|
|
11336
|
+
}
|
|
11337
|
+
|
|
11338
|
+
let watch = null;
|
|
11339
|
+
|
|
11340
|
+
if (parsed.watch && typeof parsed.watch === 'object' && !Array.isArray(parsed.watch)) {
|
|
11341
|
+
watch = {
|
|
11342
|
+
users: Array.isArray(parsed.watch.users) ? [...new Set(parsed.watch.users.filter(key => typeof key === 'string' && USER_KEY_RE.test(key)))] : [],
|
|
11343
|
+
words: Array.isArray(parsed.watch.words) ? [...new Set(parsed.watch.words.map(word => normalizeWord(String(word || ''))).filter(word => word && word.length <= 60))] : []
|
|
11344
|
+
};
|
|
11345
|
+
}
|
|
11346
|
+
|
|
10859
11347
|
return {
|
|
10860
11348
|
users,
|
|
10861
11349
|
images,
|
|
10862
11350
|
keywords,
|
|
10863
11351
|
titles,
|
|
10864
11352
|
heads,
|
|
11353
|
+
memos,
|
|
11354
|
+
watch,
|
|
10865
11355
|
scraps: importedScraps,
|
|
10866
11356
|
settings: importedSettings,
|
|
10867
11357
|
meta: {
|
|
@@ -10882,6 +11372,8 @@
|
|
|
10882
11372
|
if (data.heads) parts.push(`말머리 ${headCount}${headGalleries ? `(갤러리 ${headGalleries})` : ''}`);
|
|
10883
11373
|
if (data.images) parts.push(`이미지 ${data.images.length}`);
|
|
10884
11374
|
if (data.scraps) parts.push(`스크랩 ${data.scraps.length}`);
|
|
11375
|
+
if (data.memos && Object.keys(data.memos).length) parts.push(`메모 ${Object.keys(data.memos).length}`);
|
|
11376
|
+
if (data.watch && data.watch.users.length + data.watch.words.length) parts.push(`관심 ${data.watch.users.length + data.watch.words.length}`);
|
|
10885
11377
|
if (data.settings) parts.push('설정 포함');
|
|
10886
11378
|
|
|
10887
11379
|
return parts.join(' · ') || '비어 있음';
|
|
@@ -10944,7 +11436,30 @@
|
|
|
10944
11436
|
saveHiddenHeads();
|
|
10945
11437
|
}
|
|
10946
11438
|
|
|
10947
|
-
if (data.
|
|
11439
|
+
if (data.memos) {
|
|
11440
|
+
const now = Date.now();
|
|
11441
|
+
const current = readMemos();
|
|
11442
|
+
const next = merge ? { ...current } : {};
|
|
11443
|
+
|
|
11444
|
+
if (!merge) {
|
|
11445
|
+
Object.keys(current).forEach(key => {
|
|
11446
|
+
next[key] = { t: '', at: now };
|
|
11447
|
+
});
|
|
11448
|
+
}
|
|
11449
|
+
|
|
11450
|
+
Object.entries(data.memos).forEach(([key, item]) => {
|
|
11451
|
+
next[key] = { t: item.t, at: Math.max(now, item.at) };
|
|
11452
|
+
});
|
|
11453
|
+
writeMemos(next);
|
|
11454
|
+
}
|
|
11455
|
+
|
|
11456
|
+
if (data.watch) {
|
|
11457
|
+
const current = readWatch();
|
|
11458
|
+
|
|
11459
|
+
writeWatch(merge ? { users: [...current.users, ...data.watch.users], words: [...current.words, ...data.watch.words] } : data.watch);
|
|
11460
|
+
}
|
|
11461
|
+
|
|
11462
|
+
if (data.settings && !merge) {
|
|
10948
11463
|
settings = { ...settings, ...data.settings };
|
|
10949
11464
|
saveSettings();
|
|
10950
11465
|
applyRootSettingClasses();
|
|
@@ -11821,6 +12336,8 @@
|
|
|
11821
12336
|
{ key: 'scraps', label: '스크랩', fields: ['scraps'] },
|
|
11822
12337
|
{ key: 'recent', label: '최근 갤러리', fields: ['recent', 'recentOrder'] },
|
|
11823
12338
|
{ key: 'search', label: '검색 기록', fields: ['search'] },
|
|
12339
|
+
{ key: 'memos', label: '메모', fields: ['memos'] },
|
|
12340
|
+
{ key: 'watch', label: '관심 알림', fields: ['watch'] },
|
|
11824
12341
|
{ key: 'settings', label: '설정', fields: ['settings'] }
|
|
11825
12342
|
];
|
|
11826
12343
|
|
|
@@ -12003,7 +12520,9 @@
|
|
|
12003
12520
|
recent: collectRecentForSync(),
|
|
12004
12521
|
recentOrder: collectRecentOrderForSync(),
|
|
12005
12522
|
search: collectSearchForSync(),
|
|
12006
|
-
hits: collectHitsForSync()
|
|
12523
|
+
hits: collectHitsForSync(),
|
|
12524
|
+
memos: collectMemosForSync(),
|
|
12525
|
+
watch: readWatch()
|
|
12007
12526
|
});
|
|
12008
12527
|
|
|
12009
12528
|
const skippedFields = skip => SYNC_PARTS.filter(part => skip.includes(part.key)).flatMap(part => part.fields);
|
|
@@ -12113,7 +12632,9 @@
|
|
|
12113
12632
|
recent: mergeRecent(local.recent, base && base.recent, remote && remote.recent),
|
|
12114
12633
|
recentOrder: mergeRecentOrder(local.recentOrder, remote && remote.recentOrder),
|
|
12115
12634
|
search: mergeSearchHistory(local.search, base && base.search, remote && remote.search),
|
|
12116
|
-
hits: mergeBlockHits(local.hits, remote && remote.hits)
|
|
12635
|
+
hits: mergeBlockHits(local.hits, remote && remote.hits),
|
|
12636
|
+
memos: mergeMemos(local.memos, remote && remote.memos),
|
|
12637
|
+
watch: mergeWatch(local.watch, base && base.watch, remote && remote.watch)
|
|
12117
12638
|
};
|
|
12118
12639
|
|
|
12119
12640
|
skippedFields(skip).forEach(field => {
|
|
@@ -12149,6 +12670,12 @@
|
|
|
12149
12670
|
}
|
|
12150
12671
|
if (after.heads) diff('말머리', flatHeads(before.heads), flatHeads(after.heads));
|
|
12151
12672
|
if (after.scraps) diff('스크랩', (before.scraps || []).map(scrapKeyOf), after.scraps.map(scrapKeyOf));
|
|
12673
|
+
if (after.memos) {
|
|
12674
|
+
const memoText = memos => Object.entries(memos || {}).filter(([, item]) => item && item.t).map(([key, item]) => `${key}\u0000${item.t}`);
|
|
12675
|
+
|
|
12676
|
+
diff('메모', memoText(before.memos), memoText(after.memos));
|
|
12677
|
+
}
|
|
12678
|
+
if (after.watch) diff('관심', [...((before.watch || {}).users || []), ...((before.watch || {}).words || []).map(w => `w:${w}`)], [...(after.watch.users || []), ...(after.watch.words || []).map(w => `w:${w}`)]);
|
|
12152
12679
|
|
|
12153
12680
|
if (after.settings) {
|
|
12154
12681
|
const changed = Object.keys(pickKnownSettings(after.settings)).filter(key => JSON.stringify(after.settings[key]) !== JSON.stringify((before.settings || {})[key])).length;
|
|
@@ -12170,6 +12697,8 @@
|
|
|
12170
12697
|
delete data.recentOrder;
|
|
12171
12698
|
delete data.search;
|
|
12172
12699
|
delete data.hits;
|
|
12700
|
+
delete data.memos;
|
|
12701
|
+
delete data.watch;
|
|
12173
12702
|
|
|
12174
12703
|
return JSON.stringify(data);
|
|
12175
12704
|
};
|
|
@@ -12179,6 +12708,8 @@
|
|
|
12179
12708
|
const recentChanged = data && 'recent' in data ? applyRecentFromSync(data.recent) : false;
|
|
12180
12709
|
const searchChanged = data && 'search' in data ? applySearchFromSync(data.search) : false;
|
|
12181
12710
|
const hitsChanged = data && 'hits' in data ? applyHitsFromSync(data.hits) : false;
|
|
12711
|
+
const memosChanged = data && 'memos' in data ? applyMemosFromSync(data.memos) : false;
|
|
12712
|
+
const watchChanged = data && 'watch' in data ? applyWatchFromSync(data.watch) : false;
|
|
12182
12713
|
const clean = sanitizeBackup({ app: BACKUP_APP, ...data });
|
|
12183
12714
|
const before = blockSyncData();
|
|
12184
12715
|
|
|
@@ -12233,7 +12764,7 @@
|
|
|
12233
12764
|
refreshAfterBlockChange();
|
|
12234
12765
|
}
|
|
12235
12766
|
|
|
12236
|
-
return changed || recentChanged || orderChanged || searchChanged || hitsChanged;
|
|
12767
|
+
return changed || recentChanged || orderChanged || searchChanged || hitsChanged || memosChanged || watchChanged;
|
|
12237
12768
|
};
|
|
12238
12769
|
|
|
12239
12770
|
const saveSyncUndo = (before, incoming, summary, from) => {
|
|
@@ -12499,6 +13030,7 @@
|
|
|
12499
13030
|
|
|
12500
13031
|
const runId = ++syncRunId;
|
|
12501
13032
|
const epoch = syncEpoch;
|
|
13033
|
+
const startedAt = syncRunSince;
|
|
12502
13034
|
|
|
12503
13035
|
const key = readSyncKey();
|
|
12504
13036
|
const conf = readSyncConf();
|
|
@@ -12630,7 +13162,9 @@
|
|
|
12630
13162
|
} catch (error) {
|
|
12631
13163
|
syncPending = true;
|
|
12632
13164
|
|
|
12633
|
-
|
|
13165
|
+
const interrupted = netLeavingRecently() || (netResumedAt > 0 && netResumedAt >= startedAt) || (netHiddenAt > 0 && netHiddenAt >= startedAt);
|
|
13166
|
+
|
|
13167
|
+
if (runId !== syncRunId || (error && error.quiet) || interrupted || pageLeaving || leaving || epoch !== syncEpoch || document.visibilityState === 'hidden') {
|
|
12634
13168
|
writeSyncRt({ dirty: true });
|
|
12635
13169
|
|
|
12636
13170
|
if (runId === syncRunId && !pageLeaving && document.visibilityState !== 'hidden') {
|
|
@@ -12724,7 +13258,7 @@
|
|
|
12724
13258
|
rerenderModal();
|
|
12725
13259
|
};
|
|
12726
13260
|
|
|
12727
|
-
const SYNCED_STORAGE = [STORAGE.users, STORAGE.media, STORAGE.keywords, STORAGE.titles, STORAGE.heads, STORAGE.scraps, STORAGE.settings];
|
|
13261
|
+
const SYNCED_STORAGE = [STORAGE.users, STORAGE.media, STORAGE.keywords, STORAGE.titles, STORAGE.heads, STORAGE.scraps, STORAGE.settings, STORAGE.memos, STORAGE.watch];
|
|
12728
13262
|
|
|
12729
13263
|
storageWriteListeners.push(key => {
|
|
12730
13264
|
if (!syncApplying && SYNCED_STORAGE.includes(key) && syncReady()) {
|
|
@@ -15808,6 +16342,869 @@
|
|
|
15808
16342
|
return true;
|
|
15809
16343
|
};
|
|
15810
16344
|
|
|
16345
|
+
const MEMO_MAX = 300;
|
|
16346
|
+
const MEMO_TOMBSTONE_KEEP = 90 * 24 * 60 * 60 * 1000;
|
|
16347
|
+
|
|
16348
|
+
let memoCache = null;
|
|
16349
|
+
let watchCache = null;
|
|
16350
|
+
|
|
16351
|
+
window.addEventListener('storage', event => {
|
|
16352
|
+
if (!event.key || event.key === STORAGE.memos || event.key === STORAGE.watch) {
|
|
16353
|
+
memoCache = null;
|
|
16354
|
+
watchCache = null;
|
|
16355
|
+
refreshUserDecorations();
|
|
16356
|
+
}
|
|
16357
|
+
});
|
|
16358
|
+
|
|
16359
|
+
const rawStored = key => {
|
|
16360
|
+
try {
|
|
16361
|
+
return localStorage.getItem(key) || '';
|
|
16362
|
+
} catch {
|
|
16363
|
+
return '';
|
|
16364
|
+
}
|
|
16365
|
+
};
|
|
16366
|
+
|
|
16367
|
+
const readMemos = () => {
|
|
16368
|
+
const rawText = rawStored(STORAGE.memos);
|
|
16369
|
+
|
|
16370
|
+
if (memoCache && memoCache.raw === rawText) {
|
|
16371
|
+
return { ...memoCache.data };
|
|
16372
|
+
}
|
|
16373
|
+
|
|
16374
|
+
const raw = readObject(STORAGE.memos);
|
|
16375
|
+
const out = {};
|
|
16376
|
+
|
|
16377
|
+
Object.entries(raw || {}).forEach(([key, item]) => {
|
|
16378
|
+
if (/^(?:nick|ip|id)::.+/.test(key) && item && typeof item === 'object') {
|
|
16379
|
+
out[key] = { t: String(item.t || '').slice(0, MEMO_MAX), at: Number(item.at) || 0 };
|
|
16380
|
+
}
|
|
16381
|
+
});
|
|
16382
|
+
|
|
16383
|
+
memoCache = { raw: rawText, data: out };
|
|
16384
|
+
|
|
16385
|
+
return { ...out };
|
|
16386
|
+
};
|
|
16387
|
+
|
|
16388
|
+
const writeMemos = memos => {
|
|
16389
|
+
const now = Date.now();
|
|
16390
|
+
const out = {};
|
|
16391
|
+
|
|
16392
|
+
Object.entries(memos).forEach(([key, item]) => {
|
|
16393
|
+
if (item.t || now - item.at < MEMO_TOMBSTONE_KEEP) {
|
|
16394
|
+
out[key] = item;
|
|
16395
|
+
}
|
|
16396
|
+
});
|
|
16397
|
+
|
|
16398
|
+
memoCache = null;
|
|
16399
|
+
writeJson(STORAGE.memos, out);
|
|
16400
|
+
};
|
|
16401
|
+
|
|
16402
|
+
const memoCount = () => Object.values(readMemos()).filter(item => item.t).length;
|
|
16403
|
+
|
|
16404
|
+
const memoForKeys = keys => {
|
|
16405
|
+
const memos = readMemos();
|
|
16406
|
+
const order = ['id', 'ip', 'nick'];
|
|
16407
|
+
|
|
16408
|
+
for (const type of order) {
|
|
16409
|
+
const entry = keys.find(k => k.type === type);
|
|
16410
|
+
|
|
16411
|
+
if (entry && memos[entry.key] && memos[entry.key].t) {
|
|
16412
|
+
return { key: entry.key, text: memos[entry.key].t };
|
|
16413
|
+
}
|
|
16414
|
+
}
|
|
16415
|
+
|
|
16416
|
+
return null;
|
|
16417
|
+
};
|
|
16418
|
+
|
|
16419
|
+
const setMemo = (key, text) => {
|
|
16420
|
+
const memos = readMemos();
|
|
16421
|
+
|
|
16422
|
+
memos[key] = { t: String(text || '').trim().slice(0, MEMO_MAX), at: Date.now() };
|
|
16423
|
+
writeMemos(memos);
|
|
16424
|
+
refreshUserDecorations();
|
|
16425
|
+
};
|
|
16426
|
+
|
|
16427
|
+
const primaryUserKey = keys => (keys.find(k => k.type === 'id') || keys.find(k => k.type === 'ip') || keys[0] || {}).key || '';
|
|
16428
|
+
|
|
16429
|
+
const describeUserKey = key => {
|
|
16430
|
+
const entry = parseBlockKey(key);
|
|
16431
|
+
|
|
16432
|
+
return `${BLOCK_TYPE_LABELS[entry.type] || ''} ${entry.value}`.trim();
|
|
16433
|
+
};
|
|
16434
|
+
|
|
16435
|
+
const openMemoEditor = (keys, title) => {
|
|
16436
|
+
const key = primaryUserKey(keys);
|
|
16437
|
+
|
|
16438
|
+
if (!key) {
|
|
16439
|
+
return;
|
|
16440
|
+
}
|
|
16441
|
+
|
|
16442
|
+
document.querySelector('.dc-memo-dialog')?.remove();
|
|
16443
|
+
|
|
16444
|
+
const current = (readMemos()[key] || {}).t || '';
|
|
16445
|
+
const wrap = document.createElement('div');
|
|
16446
|
+
|
|
16447
|
+
wrap.className = 'dc-ui dc-memo-dialog';
|
|
16448
|
+
wrap.setAttribute('role', 'dialog');
|
|
16449
|
+
wrap.setAttribute('aria-modal', 'true');
|
|
16450
|
+
wrap.innerHTML =
|
|
16451
|
+
'<div class="dc-memo-box">' +
|
|
16452
|
+
`<div class="dc-memo-head"><strong>📝 메모</strong><span>${escapeHtml(title || describeUserKey(key))}</span></div>` +
|
|
16453
|
+
`<textarea maxlength="${MEMO_MAX}" placeholder="이 이용자에 대한 메모 (나만 보임 · 개수 제한 없음)"></textarea>` +
|
|
16454
|
+
`<div class="dc-memo-sub">${escapeHtml(describeUserKey(key))} 기준 · 목록·댓글의 이름 옆에 보입니다</div>` +
|
|
16455
|
+
'<div class="dc-memo-actions">' +
|
|
16456
|
+
(current ? '<button type="button" data-act="del" class="danger">지우기</button>' : '') +
|
|
16457
|
+
'<span></span><button type="button" data-act="cancel">취소</button><button type="button" data-act="save" class="primary">저장</button>' +
|
|
16458
|
+
'</div></div>';
|
|
16459
|
+
|
|
16460
|
+
const textarea = wrap.querySelector('textarea');
|
|
16461
|
+
|
|
16462
|
+
textarea.value = current;
|
|
16463
|
+
|
|
16464
|
+
const close = () => wrap.remove();
|
|
16465
|
+
|
|
16466
|
+
wrap.addEventListener('click', event => {
|
|
16467
|
+
const act = event.target instanceof Element ? event.target.closest('[data-act]')?.dataset.act : '';
|
|
16468
|
+
|
|
16469
|
+
if (event.target === wrap || act === 'cancel') {
|
|
16470
|
+
close();
|
|
16471
|
+
} else if (act === 'save') {
|
|
16472
|
+
setMemo(key, textarea.value);
|
|
16473
|
+
close();
|
|
16474
|
+
showToast(textarea.value.trim() ? '📝 메모를 저장했습니다' : '📝 메모를 지웠습니다');
|
|
16475
|
+
} else if (act === 'del') {
|
|
16476
|
+
setMemo(key, '');
|
|
16477
|
+
close();
|
|
16478
|
+
showToast('📝 메모를 지웠습니다');
|
|
16479
|
+
}
|
|
16480
|
+
});
|
|
16481
|
+
|
|
16482
|
+
textarea.addEventListener('keydown', event => {
|
|
16483
|
+
if (event.key === 'Escape') {
|
|
16484
|
+
close();
|
|
16485
|
+
}
|
|
16486
|
+
});
|
|
16487
|
+
|
|
16488
|
+
document.body.appendChild(wrap);
|
|
16489
|
+
setTimeout(() => textarea.focus(), 30);
|
|
16490
|
+
};
|
|
16491
|
+
|
|
16492
|
+
const WATCH_WORD_MAX = 60;
|
|
16493
|
+
|
|
16494
|
+
const readWatch = () => {
|
|
16495
|
+
const rawText = rawStored(STORAGE.watch);
|
|
16496
|
+
|
|
16497
|
+
if (watchCache && watchCache.raw === rawText) {
|
|
16498
|
+
return { users: [...watchCache.users], words: [...watchCache.words] };
|
|
16499
|
+
}
|
|
16500
|
+
|
|
16501
|
+
const raw = readObject(STORAGE.watch);
|
|
16502
|
+
const users = Array.isArray(raw.users) ? raw.users.filter(key => typeof key === 'string' && /^(?:nick|ip|id)::.+/.test(key)) : [];
|
|
16503
|
+
const words = Array.isArray(raw.words)
|
|
16504
|
+
? raw.words.map(word => normalizeWord(String(word || ''))).filter(word => word && word.length <= WATCH_WORD_MAX)
|
|
16505
|
+
: [];
|
|
16506
|
+
|
|
16507
|
+
watchCache = { raw: rawText, users: [...new Set(users)], words: [...new Set(words)] };
|
|
16508
|
+
|
|
16509
|
+
return { users: [...watchCache.users], words: [...watchCache.words] };
|
|
16510
|
+
};
|
|
16511
|
+
|
|
16512
|
+
const writeWatch = watch => {
|
|
16513
|
+
watchCache = null;
|
|
16514
|
+
writeJson(STORAGE.watch, { users: [...new Set(watch.users)], words: [...new Set(watch.words)] });
|
|
16515
|
+
refreshUserDecorations();
|
|
16516
|
+
};
|
|
16517
|
+
|
|
16518
|
+
const isWatchedKeys = keys => {
|
|
16519
|
+
const watched = readWatch().users;
|
|
16520
|
+
|
|
16521
|
+
return keys.some(k => watched.includes(k.key));
|
|
16522
|
+
};
|
|
16523
|
+
|
|
16524
|
+
const toggleWatchUser = keys => {
|
|
16525
|
+
const key = primaryUserKey(keys);
|
|
16526
|
+
const watch = readWatch();
|
|
16527
|
+
const on = keys.some(k => watch.users.includes(k.key));
|
|
16528
|
+
|
|
16529
|
+
watch.users = on ? watch.users.filter(item => !keys.some(k => k.key === item)) : [...watch.users, key];
|
|
16530
|
+
writeWatch(watch);
|
|
16531
|
+
showToast(on ? '☆ 관심 사용자에서 뺐습니다' : `⭐ 관심 사용자로 등록했습니다 · 새 글이 올라오면 알려 드립니다`);
|
|
16532
|
+
};
|
|
16533
|
+
|
|
16534
|
+
const addWatchWords = value => {
|
|
16535
|
+
const watch = readWatch();
|
|
16536
|
+
const words = String(value || '')
|
|
16537
|
+
.split(/[,\n]/)
|
|
16538
|
+
.map(normalizeWord)
|
|
16539
|
+
.filter(word => word && word.length <= WATCH_WORD_MAX && !watch.words.includes(word));
|
|
16540
|
+
|
|
16541
|
+
if (words.length) {
|
|
16542
|
+
watch.words = [...watch.words, ...words];
|
|
16543
|
+
writeWatch(watch);
|
|
16544
|
+
}
|
|
16545
|
+
|
|
16546
|
+
return words.length;
|
|
16547
|
+
};
|
|
16548
|
+
|
|
16549
|
+
const decoratedNodes = new Set();
|
|
16550
|
+
|
|
16551
|
+
const decorateUserNode = (nickElement, keys) => {
|
|
16552
|
+
if (!nickElement || !keys || !keys.length) {
|
|
16553
|
+
return;
|
|
16554
|
+
}
|
|
16555
|
+
|
|
16556
|
+
nickElement._dcDecorKeys = keys;
|
|
16557
|
+
decoratedNodes.add(nickElement);
|
|
16558
|
+
|
|
16559
|
+
const memo = memoForKeys(keys);
|
|
16560
|
+
const watched = isWatchedKeys(keys);
|
|
16561
|
+
let chip = nickElement._dcMemoChip;
|
|
16562
|
+
|
|
16563
|
+
nickElement.classList.toggle('dc-watch-user', watched);
|
|
16564
|
+
|
|
16565
|
+
if (!memo) {
|
|
16566
|
+
if (chip) {
|
|
16567
|
+
chip.remove();
|
|
16568
|
+
nickElement._dcMemoChip = null;
|
|
16569
|
+
}
|
|
16570
|
+
|
|
16571
|
+
return;
|
|
16572
|
+
}
|
|
16573
|
+
|
|
16574
|
+
if (!chip || !chip.isConnected) {
|
|
16575
|
+
chip = document.createElement('span');
|
|
16576
|
+
chip.className = 'dc-ui dc-memo-chip';
|
|
16577
|
+
|
|
16578
|
+
if (nickElement.tagName === 'LI') {
|
|
16579
|
+
nickElement.appendChild(chip);
|
|
16580
|
+
} else {
|
|
16581
|
+
nickElement.insertAdjacentElement('afterend', chip);
|
|
16582
|
+
}
|
|
16583
|
+
nickElement._dcMemoChip = chip;
|
|
16584
|
+
}
|
|
16585
|
+
|
|
16586
|
+
chip.textContent = `📝 ${memo.text.length > 18 ? `${memo.text.slice(0, 18)}…` : memo.text}`;
|
|
16587
|
+
chip.title = memo.text;
|
|
16588
|
+
};
|
|
16589
|
+
|
|
16590
|
+
const refreshUserDecorations = () =>
|
|
16591
|
+
withSelfMutation(() => {
|
|
16592
|
+
decoratedNodes.forEach(node => {
|
|
16593
|
+
if (!node.isConnected) {
|
|
16594
|
+
decoratedNodes.delete(node);
|
|
16595
|
+
return;
|
|
16596
|
+
}
|
|
16597
|
+
|
|
16598
|
+
decorateUserNode(node, node._dcDecorKeys);
|
|
16599
|
+
});
|
|
16600
|
+
});
|
|
16601
|
+
|
|
16602
|
+
const AUTHOR_HASH_RE = /(?:^#|&)dcx-author=([^&]+)/;
|
|
16603
|
+
|
|
16604
|
+
const authorSearchUrl = (keys, gallery = getGalleryId()) => {
|
|
16605
|
+
const nick = (keys.find(k => k.type === 'nick') || {}).value || '';
|
|
16606
|
+
const exact = keys.find(k => k.type === 'id') || keys.find(k => k.type === 'ip');
|
|
16607
|
+
|
|
16608
|
+
if (!gallery || gallery === HOME_GALLERY_KEY || !nick) {
|
|
16609
|
+
return '';
|
|
16610
|
+
}
|
|
16611
|
+
|
|
16612
|
+
let url;
|
|
16613
|
+
|
|
16614
|
+
if (IS_MOBILE_SITE) {
|
|
16615
|
+
const kind = (location.pathname.match(/^\/(board|mini|person)\//) || [])[1] || 'board';
|
|
16616
|
+
|
|
16617
|
+
url = `https://m.dcinside.com/${kind}/${encodeURIComponent(gallery)}?s_type=name&serval=${encodeURIComponent(nick)}`;
|
|
16618
|
+
} else {
|
|
16619
|
+
const prefix = (location.pathname.match(/^\/(mgallery|mini|person)\//) || [])[1];
|
|
16620
|
+
|
|
16621
|
+
url = `https://gall.dcinside.com/${prefix ? `${prefix}/` : ''}board/lists/?id=${encodeURIComponent(gallery)}&s_type=search_name&s_keyword=${encodeURIComponent(nick)}`;
|
|
16622
|
+
}
|
|
16623
|
+
|
|
16624
|
+
return exact ? `${url}#dcx-author=${encodeURIComponent(exact.key)}` : url;
|
|
16625
|
+
};
|
|
16626
|
+
|
|
16627
|
+
const readAuthorFilter = () => {
|
|
16628
|
+
const match = AUTHOR_HASH_RE.exec(location.hash);
|
|
16629
|
+
|
|
16630
|
+
if (!match) {
|
|
16631
|
+
return null;
|
|
16632
|
+
}
|
|
16633
|
+
|
|
16634
|
+
try {
|
|
16635
|
+
const key = decodeURIComponent(match[1]);
|
|
16636
|
+
|
|
16637
|
+
return /^(?:ip|id)::.+/.test(key) ? parseBlockKey(key) : null;
|
|
16638
|
+
} catch {
|
|
16639
|
+
return null;
|
|
16640
|
+
}
|
|
16641
|
+
};
|
|
16642
|
+
|
|
16643
|
+
let authorFilterOff = false;
|
|
16644
|
+
|
|
16645
|
+
const rowMatchesAuthor = (li, filter) => {
|
|
16646
|
+
const holder = li.dataset.dcProcessed ? li : li.querySelector('[data-dc-processed]');
|
|
16647
|
+
|
|
16648
|
+
if (!holder) {
|
|
16649
|
+
return null;
|
|
16650
|
+
}
|
|
16651
|
+
|
|
16652
|
+
return filter.type === 'id' ? holder.dataset.dcCode === filter.value : !holder.dataset.dcCode && holder.dataset.dcIp === filter.value;
|
|
16653
|
+
};
|
|
16654
|
+
|
|
16655
|
+
const applyAuthorFilter = () => {
|
|
16656
|
+
const filter = readAuthorFilter();
|
|
16657
|
+
const listEl = filter ? pickListElement(document) : null;
|
|
16658
|
+
|
|
16659
|
+
if (!listEl) {
|
|
16660
|
+
return;
|
|
16661
|
+
}
|
|
16662
|
+
|
|
16663
|
+
let hidden = 0;
|
|
16664
|
+
|
|
16665
|
+
withSelfMutation(() => {
|
|
16666
|
+
realListItems(listEl).forEach(li => {
|
|
16667
|
+
const match = rowMatchesAuthor(li, filter);
|
|
16668
|
+
const hide = !authorFilterOff && match === false;
|
|
16669
|
+
|
|
16670
|
+
li.classList.toggle('dc-author-other', hide);
|
|
16671
|
+
hidden += match === false ? 1 : 0;
|
|
16672
|
+
});
|
|
16673
|
+
|
|
16674
|
+
let bar = document.getElementById('dc-author-bar');
|
|
16675
|
+
|
|
16676
|
+
if (!bar) {
|
|
16677
|
+
bar = document.createElement('div');
|
|
16678
|
+
bar.id = 'dc-author-bar';
|
|
16679
|
+
bar.className = 'dc-ui dc-author-bar';
|
|
16680
|
+
streamAnchorOf(listEl).insertAdjacentElement('beforebegin', bar);
|
|
16681
|
+
bar.addEventListener('click', event => {
|
|
16682
|
+
if (event.target instanceof Element && event.target.closest('button')) {
|
|
16683
|
+
authorFilterOff = !authorFilterOff;
|
|
16684
|
+
applyAuthorFilter();
|
|
16685
|
+
}
|
|
16686
|
+
});
|
|
16687
|
+
}
|
|
16688
|
+
|
|
16689
|
+
const label = `${BLOCK_TYPE_LABELS[filter.type]} ${filter.value}`;
|
|
16690
|
+
|
|
16691
|
+
bar.innerHTML = authorFilterOff
|
|
16692
|
+
? `<span>🔍 같은 닉네임의 글을 모두 보는 중</span><button type="button">${escapeHtml(label)} 글만</button>`
|
|
16693
|
+
: `<span>🔍 <b>${escapeHtml(label)}</b> 글만 보는 중${hidden ? ` · 같은 닉네임의 다른 사람 글 ${hidden}개 숨김` : ''}</span>${hidden ? '<button type="button">모두 보기</button>' : ''}`;
|
|
16694
|
+
});
|
|
16695
|
+
};
|
|
16696
|
+
|
|
16697
|
+
const LIVE_MIN_SEC = 30;
|
|
16698
|
+
const LIVE_IDLE_STOP = 10 * 60 * 1000;
|
|
16699
|
+
const LIVE_INSERT_MAX = 30;
|
|
16700
|
+
|
|
16701
|
+
let liveTimer = null;
|
|
16702
|
+
let liveBusy = false;
|
|
16703
|
+
let liveLastCheck = 0;
|
|
16704
|
+
let liveLastFound = 0;
|
|
16705
|
+
let liveInteractAt = 0;
|
|
16706
|
+
let livePill = null;
|
|
16707
|
+
let livePending = 0;
|
|
16708
|
+
|
|
16709
|
+
const liveIntervalMs = () => Math.max(LIVE_MIN_SEC, Number(settings.liveRefreshSec) || 60) * 1000;
|
|
16710
|
+
|
|
16711
|
+
const isLiveListPage = () => {
|
|
16712
|
+
if (settings.liveRefresh === false || isPostViewPage() || readAuthorFilter()) {
|
|
16713
|
+
return false;
|
|
16714
|
+
}
|
|
16715
|
+
|
|
16716
|
+
if (!/^\/(?:mgallery\/)?(?:board|mini|person)\//.test(location.pathname)) {
|
|
16717
|
+
return false;
|
|
16718
|
+
}
|
|
16719
|
+
|
|
16720
|
+
try {
|
|
16721
|
+
const params = new URL(location.href).searchParams;
|
|
16722
|
+
|
|
16723
|
+
if (['s_type', 'serval', 's_keyword', 'search_head'].some(name => params.get(name))) {
|
|
16724
|
+
return false;
|
|
16725
|
+
}
|
|
16726
|
+
} catch {
|
|
16727
|
+
return false;
|
|
16728
|
+
}
|
|
16729
|
+
|
|
16730
|
+
return currentPageNumber() === 1 && !!pickListElement(document);
|
|
16731
|
+
};
|
|
16732
|
+
|
|
16733
|
+
const topPostNumber = listEl =>
|
|
16734
|
+
realListItems(listEl).reduce((max, li) => Math.max(max, postNumber(li.dataset.dcPostKey || listItemKey(li))), 0);
|
|
16735
|
+
|
|
16736
|
+
const hideLivePill = () => {
|
|
16737
|
+
if (livePill) {
|
|
16738
|
+
livePill.classList.remove('show');
|
|
16739
|
+
}
|
|
16740
|
+
|
|
16741
|
+
livePending = 0;
|
|
16742
|
+
};
|
|
16743
|
+
|
|
16744
|
+
const showLivePill = (count, scrolled) => {
|
|
16745
|
+
if (!livePill) {
|
|
16746
|
+
livePill = document.createElement('button');
|
|
16747
|
+
livePill.type = 'button';
|
|
16748
|
+
livePill.className = 'dc-ui dc-live-pill';
|
|
16749
|
+
livePill.addEventListener('click', () => {
|
|
16750
|
+
hideLivePill();
|
|
16751
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
16752
|
+
});
|
|
16753
|
+
document.body.appendChild(livePill);
|
|
16754
|
+
}
|
|
16755
|
+
|
|
16756
|
+
livePending = scrolled ? livePending + count : count;
|
|
16757
|
+
livePill.textContent = scrolled ? `↑ 새 글 ${livePending}개` : `✨ 새 글 ${count}개`;
|
|
16758
|
+
livePill.classList.add('show');
|
|
16759
|
+
clearTimeout(livePill._timer);
|
|
16760
|
+
|
|
16761
|
+
if (!scrolled) {
|
|
16762
|
+
livePill._timer = setTimeout(hideLivePill, 2600);
|
|
16763
|
+
}
|
|
16764
|
+
};
|
|
16765
|
+
|
|
16766
|
+
const rowWriterKeys = li => {
|
|
16767
|
+
const holder = li.dataset.dcProcessed ? li : li.querySelector('[data-dc-processed]');
|
|
16768
|
+
|
|
16769
|
+
return holder
|
|
16770
|
+
? buildBlockKeys({ nickname: holder.dataset.dcNick || '', ip: holder.dataset.dcIp || '', code: holder.dataset.dcCode || '' })
|
|
16771
|
+
: [];
|
|
16772
|
+
};
|
|
16773
|
+
|
|
16774
|
+
const rowTitle = li => cleanText((li.querySelector('.subjectin, .gall_tit a:not(.reply_numbox)') || {}).textContent || '');
|
|
16775
|
+
|
|
16776
|
+
const rowIsShown = li =>
|
|
16777
|
+
li.isConnected &&
|
|
16778
|
+
!li.classList.contains('dc-user-blocked') &&
|
|
16779
|
+
!li.classList.contains('dc-head-hidden') &&
|
|
16780
|
+
!li.classList.contains('dc-author-other') &&
|
|
16781
|
+
getComputedStyle(li).display !== 'none';
|
|
16782
|
+
|
|
16783
|
+
const notifyWatchHits = rows => {
|
|
16784
|
+
if (settings.watchAlerts === false || !rows.length) {
|
|
16785
|
+
return;
|
|
16786
|
+
}
|
|
16787
|
+
|
|
16788
|
+
const watch = readWatch();
|
|
16789
|
+
|
|
16790
|
+
if (!watch.users.length && !watch.words.length) {
|
|
16791
|
+
return;
|
|
16792
|
+
}
|
|
16793
|
+
|
|
16794
|
+
const gallery = getGalleryId();
|
|
16795
|
+
const seen = readObject(STORAGE.liveSeen);
|
|
16796
|
+
const seenNo = Number(seen[gallery]) || 0;
|
|
16797
|
+
let maxNo = seenNo;
|
|
16798
|
+
const hits = [];
|
|
16799
|
+
|
|
16800
|
+
rows.forEach(li => {
|
|
16801
|
+
const no = postNumber(li.dataset.dcPostKey || listItemKey(li));
|
|
16802
|
+
|
|
16803
|
+
if (!no || no <= seenNo) {
|
|
16804
|
+
return;
|
|
16805
|
+
}
|
|
16806
|
+
|
|
16807
|
+
maxNo = Math.max(maxNo, no);
|
|
16808
|
+
|
|
16809
|
+
const title = rowTitle(li);
|
|
16810
|
+
const lower = title.toLowerCase();
|
|
16811
|
+
const word = watch.words.find(item => lower.includes(item.toLowerCase()));
|
|
16812
|
+
const keys = rowWriterKeys(li);
|
|
16813
|
+
const user = keys.find(k => watch.users.includes(k.key));
|
|
16814
|
+
|
|
16815
|
+
if (word || user) {
|
|
16816
|
+
li.classList.add('dc-live-watch');
|
|
16817
|
+
hits.push({ li, title, reason: word ? `키워드 '${word}'` : `관심 사용자 ${user.value}` });
|
|
16818
|
+
}
|
|
16819
|
+
});
|
|
16820
|
+
|
|
16821
|
+
if (maxNo > seenNo) {
|
|
16822
|
+
writeJson(STORAGE.liveSeen, { ...seen, [gallery]: maxNo });
|
|
16823
|
+
}
|
|
16824
|
+
|
|
16825
|
+
if (!hits.length) {
|
|
16826
|
+
return;
|
|
16827
|
+
}
|
|
16828
|
+
|
|
16829
|
+
const first = hits[0];
|
|
16830
|
+
const link = first.li.querySelector(POST_LINK_SELECTOR);
|
|
16831
|
+
const more = hits.length > 1 ? ` 외 ${hits.length - 1}개` : '';
|
|
16832
|
+
|
|
16833
|
+
showToast(`🔔 ${first.reason} · ${first.title}${more}`, {
|
|
16834
|
+
actionLabel: '보기',
|
|
16835
|
+
onAction: () => {
|
|
16836
|
+
if (link) {
|
|
16837
|
+
link.click();
|
|
16838
|
+
}
|
|
16839
|
+
},
|
|
16840
|
+
duration: 7000
|
|
16841
|
+
});
|
|
16842
|
+
|
|
16843
|
+
if (canSystemNotify() && document.visibilityState === 'hidden') {
|
|
16844
|
+
try {
|
|
16845
|
+
const note = new window.Notification(`디시 관심 알림 · ${first.reason}`, { body: `${first.title}${more}`, tag: `dcx-watch-${gallery}` });
|
|
16846
|
+
|
|
16847
|
+
note.onclick = () => {
|
|
16848
|
+
window.focus();
|
|
16849
|
+
note.close();
|
|
16850
|
+
|
|
16851
|
+
if (link) {
|
|
16852
|
+
link.click();
|
|
16853
|
+
}
|
|
16854
|
+
};
|
|
16855
|
+
} catch {
|
|
16856
|
+
}
|
|
16857
|
+
}
|
|
16858
|
+
};
|
|
16859
|
+
|
|
16860
|
+
const checkLiveList = async () => {
|
|
16861
|
+
if (liveBusy || !isLiveListPage() || document.visibilityState === 'hidden' || netPausedUntil()) {
|
|
16862
|
+
return;
|
|
16863
|
+
}
|
|
16864
|
+
|
|
16865
|
+
if (Date.now() - liveInteractAt > LIVE_IDLE_STOP) {
|
|
16866
|
+
return;
|
|
16867
|
+
}
|
|
16868
|
+
|
|
16869
|
+
const listEl = pickListElement(document);
|
|
16870
|
+
|
|
16871
|
+
if (!listEl) {
|
|
16872
|
+
return;
|
|
16873
|
+
}
|
|
16874
|
+
|
|
16875
|
+
liveBusy = true;
|
|
16876
|
+
liveLastCheck = Date.now();
|
|
16877
|
+
|
|
16878
|
+
try {
|
|
16879
|
+
const html = await siteFetchText(buildPageUrl(1), { credentials: 'same-origin', cache: 'no-store' }, { auto: true });
|
|
16880
|
+
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
16881
|
+
const fetched = pickListElement(doc);
|
|
16882
|
+
|
|
16883
|
+
if (!fetched || !listEl.isConnected) {
|
|
16884
|
+
return;
|
|
16885
|
+
}
|
|
16886
|
+
|
|
16887
|
+
const existing = new Set(realListItems(listEl).map(li => li.dataset.dcPostKey || listItemKey(li)));
|
|
16888
|
+
const topNo = topPostNumber(listEl);
|
|
16889
|
+
const fresh = [...fetched.children]
|
|
16890
|
+
.filter(node => isListItemNode(node) && !node.classList.contains('adv-inner'))
|
|
16891
|
+
.filter(node => {
|
|
16892
|
+
const key = listItemKey(node);
|
|
16893
|
+
|
|
16894
|
+
return key && !existing.has(key) && postNumber(key) > topNo;
|
|
16895
|
+
})
|
|
16896
|
+
.slice(0, LIVE_INSERT_MAX);
|
|
16897
|
+
|
|
16898
|
+
if (!fresh.length || !topNo) {
|
|
16899
|
+
return;
|
|
16900
|
+
}
|
|
16901
|
+
|
|
16902
|
+
const anchor = realListItems(listEl).find(li => postNumber(li.dataset.dcPostKey || listItemKey(li)) === topNo);
|
|
16903
|
+
|
|
16904
|
+
if (!anchor) {
|
|
16905
|
+
return;
|
|
16906
|
+
}
|
|
16907
|
+
|
|
16908
|
+
const scrolled = window.scrollY > 120;
|
|
16909
|
+
const before = anchor.getBoundingClientRect().top;
|
|
16910
|
+
|
|
16911
|
+
fresh.forEach(li => {
|
|
16912
|
+
const node = document.adoptNode(li);
|
|
16913
|
+
|
|
16914
|
+
prepareAppendedItem(node);
|
|
16915
|
+
node.classList.add('dc-live-new');
|
|
16916
|
+
listEl.insertBefore(node, anchor);
|
|
16917
|
+
});
|
|
16918
|
+
|
|
16919
|
+
processElements();
|
|
16920
|
+
|
|
16921
|
+
if (scrolled) {
|
|
16922
|
+
const shift = anchor.getBoundingClientRect().top - before;
|
|
16923
|
+
|
|
16924
|
+
if (shift) {
|
|
16925
|
+
window.scrollBy(0, shift);
|
|
16926
|
+
}
|
|
16927
|
+
}
|
|
16928
|
+
|
|
16929
|
+
const shown = fresh.filter(rowIsShown);
|
|
16930
|
+
|
|
16931
|
+
liveLastFound = Date.now();
|
|
16932
|
+
|
|
16933
|
+
if (shown.length) {
|
|
16934
|
+
showLivePill(shown.length, scrolled);
|
|
16935
|
+
}
|
|
16936
|
+
|
|
16937
|
+
notifyWatchHits(shown);
|
|
16938
|
+
setTimeout(() => fresh.forEach(li => li.classList.remove('dc-live-new')), 4000);
|
|
16939
|
+
} catch {
|
|
16940
|
+
} finally {
|
|
16941
|
+
liveBusy = false;
|
|
16942
|
+
}
|
|
16943
|
+
};
|
|
16944
|
+
|
|
16945
|
+
const scheduleLive = () => {
|
|
16946
|
+
clearTimeout(liveTimer);
|
|
16947
|
+
|
|
16948
|
+
if (!isLiveListPage() || document.visibilityState === 'hidden' || Date.now() - liveInteractAt > LIVE_IDLE_STOP) {
|
|
16949
|
+
return;
|
|
16950
|
+
}
|
|
16951
|
+
|
|
16952
|
+
liveTimer = setTimeout(async () => {
|
|
16953
|
+
await checkLiveList();
|
|
16954
|
+
scheduleLive();
|
|
16955
|
+
}, liveIntervalMs());
|
|
16956
|
+
};
|
|
16957
|
+
|
|
16958
|
+
const liveStatusText = () => {
|
|
16959
|
+
if (settings.liveRefresh === false) {
|
|
16960
|
+
return null;
|
|
16961
|
+
}
|
|
16962
|
+
|
|
16963
|
+
if (!isLiveListPage()) {
|
|
16964
|
+
return '갤러리 목록 1페이지에서 동작합니다.';
|
|
16965
|
+
}
|
|
16966
|
+
|
|
16967
|
+
return liveLastCheck
|
|
16968
|
+
? `마지막 확인 ${timeAgo(liveLastCheck)}${liveLastFound ? ` · 마지막 새 글 ${timeAgo(liveLastFound)}` : ''}`
|
|
16969
|
+
: '곧 첫 확인을 합니다.';
|
|
16970
|
+
};
|
|
16971
|
+
|
|
16972
|
+
const initLive = () => {
|
|
16973
|
+
const touched = () => {
|
|
16974
|
+
const wasIdle = Date.now() - liveInteractAt > LIVE_IDLE_STOP;
|
|
16975
|
+
|
|
16976
|
+
liveInteractAt = Date.now();
|
|
16977
|
+
|
|
16978
|
+
if (wasIdle) {
|
|
16979
|
+
if (Date.now() - liveLastCheck > liveIntervalMs()) {
|
|
16980
|
+
clearTimeout(liveTimer);
|
|
16981
|
+
liveTimer = setTimeout(() => checkLiveList().then(scheduleLive), 1500);
|
|
16982
|
+
} else {
|
|
16983
|
+
scheduleLive();
|
|
16984
|
+
}
|
|
16985
|
+
}
|
|
16986
|
+
};
|
|
16987
|
+
|
|
16988
|
+
['pointerdown', 'keydown', 'wheel', 'touchstart'].forEach(type => window.addEventListener(type, touched, { passive: true, capture: true }));
|
|
16989
|
+
window.addEventListener('scroll', () => {
|
|
16990
|
+
touched();
|
|
16991
|
+
|
|
16992
|
+
if (livePill && window.scrollY < 60 && livePending) {
|
|
16993
|
+
hideLivePill();
|
|
16994
|
+
}
|
|
16995
|
+
}, { passive: true });
|
|
16996
|
+
|
|
16997
|
+
document.addEventListener('visibilitychange', () => {
|
|
16998
|
+
if (document.visibilityState === 'visible') {
|
|
16999
|
+
if (!liveInteractAt) {
|
|
17000
|
+
return;
|
|
17001
|
+
}
|
|
17002
|
+
|
|
17003
|
+
liveInteractAt = Date.now();
|
|
17004
|
+
|
|
17005
|
+
if (Date.now() - liveLastCheck > liveIntervalMs()) {
|
|
17006
|
+
clearTimeout(liveTimer);
|
|
17007
|
+
liveTimer = setTimeout(() => checkLiveList().then(scheduleLive), 2500);
|
|
17008
|
+
} else {
|
|
17009
|
+
scheduleLive();
|
|
17010
|
+
}
|
|
17011
|
+
} else {
|
|
17012
|
+
clearTimeout(liveTimer);
|
|
17013
|
+
}
|
|
17014
|
+
});
|
|
17015
|
+
|
|
17016
|
+
window.addEventListener('hashchange', () => {
|
|
17017
|
+
authorFilterOff = false;
|
|
17018
|
+
applyAuthorFilter();
|
|
17019
|
+
});
|
|
17020
|
+
|
|
17021
|
+
if (Number.isFinite(window.__dcEnhancerNetScale)) {
|
|
17022
|
+
window.__dcEnhancerLiveCheck = () => {
|
|
17023
|
+
liveInteractAt = Date.now();
|
|
17024
|
+
|
|
17025
|
+
return checkLiveList();
|
|
17026
|
+
};
|
|
17027
|
+
}
|
|
17028
|
+
};
|
|
17029
|
+
|
|
17030
|
+
const collectMemosForSync = () => readMemos();
|
|
17031
|
+
|
|
17032
|
+
const mergeMemos = (local = {}, remote) => {
|
|
17033
|
+
const out = { ...(remote && typeof remote === 'object' ? remote : {}) };
|
|
17034
|
+
|
|
17035
|
+
Object.entries(local || {}).forEach(([key, item]) => {
|
|
17036
|
+
if (!out[key] || (Number(item.at) || 0) >= (Number(out[key].at) || 0)) {
|
|
17037
|
+
out[key] = item;
|
|
17038
|
+
}
|
|
17039
|
+
});
|
|
17040
|
+
|
|
17041
|
+
return out;
|
|
17042
|
+
};
|
|
17043
|
+
|
|
17044
|
+
const applyMemosFromSync = memos => {
|
|
17045
|
+
if (!memos || typeof memos !== 'object' || Array.isArray(memos)) {
|
|
17046
|
+
return false;
|
|
17047
|
+
}
|
|
17048
|
+
|
|
17049
|
+
const next = JSON.stringify(mergeMemos(readMemos(), memos));
|
|
17050
|
+
|
|
17051
|
+
if (next === JSON.stringify(readMemos())) {
|
|
17052
|
+
return false;
|
|
17053
|
+
}
|
|
17054
|
+
|
|
17055
|
+
memoCache = null;
|
|
17056
|
+
writeJson(STORAGE.memos, JSON.parse(next));
|
|
17057
|
+
refreshUserDecorations();
|
|
17058
|
+
|
|
17059
|
+
return true;
|
|
17060
|
+
};
|
|
17061
|
+
|
|
17062
|
+
const mergeWatch = (local = {}, base, remote) => ({
|
|
17063
|
+
users: mergeList(local.users || [], base ? base.users || [] : null, remote ? remote.users || [] : undefined),
|
|
17064
|
+
words: mergeList(local.words || [], base ? base.words || [] : null, remote ? remote.words || [] : undefined)
|
|
17065
|
+
});
|
|
17066
|
+
|
|
17067
|
+
const applyWatchFromSync = watch => {
|
|
17068
|
+
if (!watch || typeof watch !== 'object') {
|
|
17069
|
+
return false;
|
|
17070
|
+
}
|
|
17071
|
+
|
|
17072
|
+
const next = { users: Array.isArray(watch.users) ? watch.users : [], words: Array.isArray(watch.words) ? watch.words : [] };
|
|
17073
|
+
|
|
17074
|
+
if (JSON.stringify(next) === JSON.stringify(readWatch())) {
|
|
17075
|
+
return false;
|
|
17076
|
+
}
|
|
17077
|
+
|
|
17078
|
+
watchCache = null;
|
|
17079
|
+
writeJson(STORAGE.watch, next);
|
|
17080
|
+
refreshUserDecorations();
|
|
17081
|
+
|
|
17082
|
+
return true;
|
|
17083
|
+
};
|
|
17084
|
+
|
|
17085
|
+
const renderWatchView = area => {
|
|
17086
|
+
const watch = readWatch();
|
|
17087
|
+
const memos = Object.entries(readMemos()).filter(([, item]) => item.t).sort((a, b) => b[1].at - a[1].at);
|
|
17088
|
+
|
|
17089
|
+
area.appendChild(
|
|
17090
|
+
makeNote(
|
|
17091
|
+
'관심 키워드·관심 사용자의 새 글이 목록에 올라오면 알려 드립니다 (설정 › 목록 보기 › 새 글 자동으로 이어 붙이기). 메모는 개수 제한 없이 이 기기에 저장되고 동기화됩니다.'
|
|
17092
|
+
)
|
|
17093
|
+
);
|
|
17094
|
+
|
|
17095
|
+
area.appendChild(makeSectionTitle(`🔔 관심 키워드 (${watch.words.length})`));
|
|
17096
|
+
area.appendChild(
|
|
17097
|
+
makeInputRow('제목에 들어갈 단어 (쉼표로 여러 개)', '추가', value => {
|
|
17098
|
+
addWatchWords(value);
|
|
17099
|
+
rerenderModal();
|
|
17100
|
+
}, { maxLength: 300 })
|
|
17101
|
+
);
|
|
17102
|
+
|
|
17103
|
+
if (!watch.words.length) {
|
|
17104
|
+
area.appendChild(makeEmpty('관심 키워드가 없습니다.'));
|
|
17105
|
+
}
|
|
17106
|
+
|
|
17107
|
+
watch.words.forEach(word => {
|
|
17108
|
+
area.appendChild(
|
|
17109
|
+
makeRow({
|
|
17110
|
+
title: word,
|
|
17111
|
+
chip: '키워드',
|
|
17112
|
+
chipClass: 'scope',
|
|
17113
|
+
actions: [{
|
|
17114
|
+
label: '빼기',
|
|
17115
|
+
className: 'on',
|
|
17116
|
+
onClick: () => {
|
|
17117
|
+
const next = readWatch();
|
|
17118
|
+
|
|
17119
|
+
next.words = next.words.filter(item => item !== word);
|
|
17120
|
+
writeWatch(next);
|
|
17121
|
+
rerenderModal();
|
|
17122
|
+
}
|
|
17123
|
+
}]
|
|
17124
|
+
})
|
|
17125
|
+
);
|
|
17126
|
+
});
|
|
17127
|
+
|
|
17128
|
+
area.appendChild(makeSectionTitle(`⭐ 관심 사용자 (${watch.users.length})`));
|
|
17129
|
+
|
|
17130
|
+
if (!watch.users.length) {
|
|
17131
|
+
area.appendChild(makeEmpty('닉네임을 누른 메뉴에서 ⭐ 관심 사용자로 등록할 수 있습니다.'));
|
|
17132
|
+
}
|
|
17133
|
+
|
|
17134
|
+
watch.users.forEach(key => {
|
|
17135
|
+
const entry = parseBlockKey(key);
|
|
17136
|
+
const memo = (readMemos()[key] || {}).t;
|
|
17137
|
+
|
|
17138
|
+
area.appendChild(
|
|
17139
|
+
makeRow({
|
|
17140
|
+
title: entry.value,
|
|
17141
|
+
chip: BLOCK_TYPE_LABELS[entry.type],
|
|
17142
|
+
chipClass: BLOCK_TYPE_CLASS[entry.type],
|
|
17143
|
+
meta: memo ? `📝 ${memo}` : '',
|
|
17144
|
+
actions: [{
|
|
17145
|
+
label: '빼기',
|
|
17146
|
+
className: 'on',
|
|
17147
|
+
onClick: () => {
|
|
17148
|
+
const next = readWatch();
|
|
17149
|
+
|
|
17150
|
+
next.users = next.users.filter(item => item !== key);
|
|
17151
|
+
writeWatch(next);
|
|
17152
|
+
rerenderModal();
|
|
17153
|
+
}
|
|
17154
|
+
}]
|
|
17155
|
+
})
|
|
17156
|
+
);
|
|
17157
|
+
});
|
|
17158
|
+
|
|
17159
|
+
area.appendChild(makeSectionTitle(`📝 이용자 메모 (${memos.length})`));
|
|
17160
|
+
|
|
17161
|
+
const filterRow = makeFilterRow(memos.length);
|
|
17162
|
+
|
|
17163
|
+
if (filterRow) {
|
|
17164
|
+
area.appendChild(filterRow);
|
|
17165
|
+
}
|
|
17166
|
+
|
|
17167
|
+
if (!memos.length) {
|
|
17168
|
+
area.appendChild(makeEmpty('닉네임을 누른 메뉴의 📝 메모로 남길 수 있습니다.'));
|
|
17169
|
+
}
|
|
17170
|
+
|
|
17171
|
+
memos.forEach(([key, item]) => {
|
|
17172
|
+
const entry = parseBlockKey(key);
|
|
17173
|
+
|
|
17174
|
+
area.appendChild(
|
|
17175
|
+
makeRow({
|
|
17176
|
+
title: item.t,
|
|
17177
|
+
chip: `${BLOCK_TYPE_LABELS[entry.type]} ${entry.value}`,
|
|
17178
|
+
chipClass: BLOCK_TYPE_CLASS[entry.type],
|
|
17179
|
+
meta: timeAgo(item.at),
|
|
17180
|
+
actions: [
|
|
17181
|
+
{ label: '고치기', onClick: () => openMemoEditor([{ ...entry, key }]) },
|
|
17182
|
+
{
|
|
17183
|
+
label: '지우기',
|
|
17184
|
+
className: 'on',
|
|
17185
|
+
onClick: () => {
|
|
17186
|
+
setMemo(key, '');
|
|
17187
|
+
rerenderModal();
|
|
17188
|
+
}
|
|
17189
|
+
}
|
|
17190
|
+
]
|
|
17191
|
+
})
|
|
17192
|
+
);
|
|
17193
|
+
});
|
|
17194
|
+
|
|
17195
|
+
applyListFilter(area);
|
|
17196
|
+
};
|
|
17197
|
+
|
|
17198
|
+
(() => {
|
|
17199
|
+
const tail = ['recent', 'settings'].filter(key => MODAL_VIEWS[key]).map(key => [key, MODAL_VIEWS[key]]);
|
|
17200
|
+
|
|
17201
|
+
tail.forEach(([key]) => delete MODAL_VIEWS[key]);
|
|
17202
|
+
MODAL_VIEWS.watch = { title: '관심·메모', tab: '🔔 관심', render: renderWatchView };
|
|
17203
|
+
tail.forEach(([key, view]) => {
|
|
17204
|
+
MODAL_VIEWS[key] = view;
|
|
17205
|
+
});
|
|
17206
|
+
})();
|
|
17207
|
+
|
|
15811
17208
|
const FAB_DRAG_THRESHOLD = 8;
|
|
15812
17209
|
const FAB_EDGE_MARGIN = 6;
|
|
15813
17210
|
|
|
@@ -16785,6 +18182,7 @@
|
|
|
16785
18182
|
initSiteAlarms();
|
|
16786
18183
|
initKeyboardShortcuts();
|
|
16787
18184
|
initHoverPreview();
|
|
18185
|
+
initLive();
|
|
16788
18186
|
restoreListSnapshot();
|
|
16789
18187
|
processElements();
|
|
16790
18188
|
|