@kangsicj/dcinsideenhancer 1.3.7 → 1.3.9
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 +1462 -41
- 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.9
|
|
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.9';
|
|
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;
|
|
@@ -835,6 +849,13 @@
|
|
|
835
849
|
opacity: .5;
|
|
836
850
|
}
|
|
837
851
|
|
|
852
|
+
.dc-media-hidden + .img-comment,
|
|
853
|
+
.dc-media-hidden + .btn-imgcomment,
|
|
854
|
+
.dc-media-hidden + .dc-media-tools + .img-comment,
|
|
855
|
+
.dc-media-hidden + .dc-media-tools + .btn-imgcomment {
|
|
856
|
+
display: none !important;
|
|
857
|
+
}
|
|
858
|
+
|
|
838
859
|
.layer-zoom-popup .dc-media-tools {
|
|
839
860
|
display: none !important;
|
|
840
861
|
}
|
|
@@ -1021,6 +1042,11 @@
|
|
|
1021
1042
|
#dc-custom-modal .dc-modal-tab .dc-tab-ico { flex: 0 0 auto; font-size: 13px; line-height: 1; }
|
|
1022
1043
|
#dc-custom-modal .dc-modal-tab .dc-tab-txt { min-width: 0; overflow: hidden; text-overflow: ellipsis; line-height: 1.2; }
|
|
1023
1044
|
|
|
1045
|
+
@media (max-width: 520px) {
|
|
1046
|
+
#dc-custom-modal .dc-modal-tab { flex-direction: column; gap: 2px; height: 44px; font-size: 11px; letter-spacing: -.5px; }
|
|
1047
|
+
#dc-custom-modal .dc-modal-tab .dc-tab-ico { font-size: 15px; }
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1024
1050
|
#dc-custom-modal .dc-section-title {
|
|
1025
1051
|
display: flex;
|
|
1026
1052
|
align-items: center;
|
|
@@ -2937,6 +2963,160 @@
|
|
|
2937
2963
|
animation: none !important;
|
|
2938
2964
|
}
|
|
2939
2965
|
}
|
|
2966
|
+
|
|
2967
|
+
table.gall_list .gall_writer .addbox { max-width: none !important; flex-wrap: wrap; }
|
|
2968
|
+
table.gall_list .gall_writer .nickname,
|
|
2969
|
+
table.gall_list .gall_writer .addbox .nickname {
|
|
2970
|
+
max-width: 100% !important;
|
|
2971
|
+
white-space: normal !important;
|
|
2972
|
+
overflow: visible !important;
|
|
2973
|
+
text-overflow: clip !important;
|
|
2974
|
+
word-break: break-all;
|
|
2975
|
+
flex-shrink: 1 !important;
|
|
2976
|
+
}
|
|
2977
|
+
table.gall_list .gall_writer .nickname em { line-height: 15px !important; }
|
|
2978
|
+
|
|
2979
|
+
.dc-memo-chip {
|
|
2980
|
+
display: inline-block;
|
|
2981
|
+
max-width: 12em;
|
|
2982
|
+
margin-left: 4px;
|
|
2983
|
+
padding: 0 5px;
|
|
2984
|
+
border-radius: 6px;
|
|
2985
|
+
background: var(--dc-accent-soft);
|
|
2986
|
+
color: var(--dc-accent-ink);
|
|
2987
|
+
font-size: 11px;
|
|
2988
|
+
line-height: 16px;
|
|
2989
|
+
vertical-align: middle;
|
|
2990
|
+
white-space: nowrap;
|
|
2991
|
+
overflow: hidden;
|
|
2992
|
+
text-overflow: ellipsis;
|
|
2993
|
+
font-weight: 500;
|
|
2994
|
+
}
|
|
2995
|
+
.ginfo li .dc-memo-chip { float: none; }
|
|
2996
|
+
.dc-watch-user::before {
|
|
2997
|
+
content: "⭐";
|
|
2998
|
+
margin-right: 2px;
|
|
2999
|
+
font-size: .85em;
|
|
3000
|
+
}
|
|
3001
|
+
.dc-memo-dialog {
|
|
3002
|
+
position: fixed;
|
|
3003
|
+
inset: 0;
|
|
3004
|
+
z-index: 2147483200;
|
|
3005
|
+
display: flex;
|
|
3006
|
+
align-items: center;
|
|
3007
|
+
justify-content: center;
|
|
3008
|
+
padding: 16px;
|
|
3009
|
+
background: var(--dc-overlay);
|
|
3010
|
+
}
|
|
3011
|
+
.dc-memo-box {
|
|
3012
|
+
width: min(420px, 100%);
|
|
3013
|
+
padding: 16px;
|
|
3014
|
+
border-radius: 14px;
|
|
3015
|
+
background: var(--dc-surface);
|
|
3016
|
+
color: var(--dc-text);
|
|
3017
|
+
box-shadow: var(--dc-shadow);
|
|
3018
|
+
font-size: 14px;
|
|
3019
|
+
}
|
|
3020
|
+
.dc-memo-head { display: flex; gap: 8px; align-items: baseline; margin-bottom: 10px; min-width: 0; }
|
|
3021
|
+
.dc-memo-head span { color: var(--dc-text-dim); font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
3022
|
+
.dc-memo-box textarea {
|
|
3023
|
+
box-sizing: border-box;
|
|
3024
|
+
width: 100%;
|
|
3025
|
+
min-height: 96px;
|
|
3026
|
+
padding: 10px;
|
|
3027
|
+
border: 1px solid var(--dc-border);
|
|
3028
|
+
border-radius: 10px;
|
|
3029
|
+
background: var(--dc-surface-2);
|
|
3030
|
+
color: var(--dc-text);
|
|
3031
|
+
font: inherit;
|
|
3032
|
+
font-size: 16px;
|
|
3033
|
+
resize: vertical;
|
|
3034
|
+
}
|
|
3035
|
+
.dc-memo-sub { margin-top: 6px; color: var(--dc-text-dim); font-size: 12px; }
|
|
3036
|
+
.dc-memo-actions { display: flex; gap: 8px; align-items: center; margin-top: 12px; }
|
|
3037
|
+
.dc-memo-actions span { flex: 1; }
|
|
3038
|
+
.dc-memo-actions button {
|
|
3039
|
+
padding: 8px 14px;
|
|
3040
|
+
border: 1px solid var(--dc-border);
|
|
3041
|
+
border-radius: 10px;
|
|
3042
|
+
background: var(--dc-surface);
|
|
3043
|
+
color: var(--dc-text);
|
|
3044
|
+
font-size: 14px;
|
|
3045
|
+
cursor: pointer;
|
|
3046
|
+
}
|
|
3047
|
+
.dc-memo-actions button.primary { border-color: var(--dc-accent); background: var(--dc-accent); color: var(--dc-on-accent); }
|
|
3048
|
+
.dc-memo-actions button.danger { border-color: transparent; background: var(--dc-danger-soft); color: var(--dc-danger-ink); }
|
|
3049
|
+
.dc-block-popover .dc-pop-extra { display: flex; gap: 4px; margin-top: 6px; }
|
|
3050
|
+
.dc-block-popover .dc-pop-mini {
|
|
3051
|
+
flex: 1;
|
|
3052
|
+
padding: 6px 4px;
|
|
3053
|
+
border: 1px solid var(--dc-border);
|
|
3054
|
+
border-radius: 8px;
|
|
3055
|
+
background: transparent;
|
|
3056
|
+
color: var(--dc-text);
|
|
3057
|
+
font-size: 12px;
|
|
3058
|
+
white-space: nowrap;
|
|
3059
|
+
cursor: pointer;
|
|
3060
|
+
}
|
|
3061
|
+
.dc-block-popover .dc-pop-mini:hover { background: var(--dc-accent-soft); }
|
|
3062
|
+
.dc-block-popover .dc-pop-mini.on { border-color: var(--dc-accent); color: var(--dc-accent-ink); }
|
|
3063
|
+
.dc-author-bar {
|
|
3064
|
+
display: flex;
|
|
3065
|
+
gap: 8px;
|
|
3066
|
+
align-items: center;
|
|
3067
|
+
justify-content: space-between;
|
|
3068
|
+
margin: 6px 0;
|
|
3069
|
+
padding: 8px 12px;
|
|
3070
|
+
border-radius: 10px;
|
|
3071
|
+
background: var(--dc-accent-soft);
|
|
3072
|
+
color: var(--dc-text);
|
|
3073
|
+
font-size: 13px;
|
|
3074
|
+
line-height: 1.4;
|
|
3075
|
+
}
|
|
3076
|
+
.dc-author-bar button {
|
|
3077
|
+
flex: none;
|
|
3078
|
+
padding: 4px 10px;
|
|
3079
|
+
border: 1px solid var(--dc-border);
|
|
3080
|
+
border-radius: 8px;
|
|
3081
|
+
background: var(--dc-surface);
|
|
3082
|
+
color: var(--dc-accent-ink);
|
|
3083
|
+
font-size: 12px;
|
|
3084
|
+
cursor: pointer;
|
|
3085
|
+
}
|
|
3086
|
+
.dc-author-other { display: none !important; }
|
|
3087
|
+
html.dc-peek .dc-author-other { display: revert !important; opacity: .45; }
|
|
3088
|
+
.dc-live-pill {
|
|
3089
|
+
position: fixed;
|
|
3090
|
+
top: calc(env(safe-area-inset-top, 0px) + 64px);
|
|
3091
|
+
left: 50%;
|
|
3092
|
+
z-index: 2147483000;
|
|
3093
|
+
padding: 8px 16px;
|
|
3094
|
+
border: none;
|
|
3095
|
+
border-radius: 999px;
|
|
3096
|
+
background: var(--dc-accent);
|
|
3097
|
+
color: var(--dc-on-accent);
|
|
3098
|
+
box-shadow: var(--dc-shadow-sm);
|
|
3099
|
+
font-size: 13px;
|
|
3100
|
+
font-weight: 600;
|
|
3101
|
+
opacity: 0;
|
|
3102
|
+
transform: translate(-50%, -12px);
|
|
3103
|
+
pointer-events: none;
|
|
3104
|
+
transition: opacity .2s ease, transform .2s ease;
|
|
3105
|
+
cursor: pointer;
|
|
3106
|
+
}
|
|
3107
|
+
.dc-live-pill.show { opacity: 1; transform: translate(-50%, 0); pointer-events: auto; }
|
|
3108
|
+
.dc-live-new { animation: dc-live-flash 3.5s ease-out; }
|
|
3109
|
+
.dc-live-new > td { animation: dc-live-flash 3.5s ease-out; }
|
|
3110
|
+
@keyframes dc-live-flash {
|
|
3111
|
+
0%, 40% { background-color: rgba(91,110,214,.16); }
|
|
3112
|
+
100% { background-color: transparent; }
|
|
3113
|
+
}
|
|
3114
|
+
.dc-live-watch .subjectin,
|
|
3115
|
+
.dc-live-watch .gall_tit a:first-child { font-weight: 700; }
|
|
3116
|
+
@media (prefers-reduced-motion: reduce) {
|
|
3117
|
+
.dc-live-pill { transition: none; }
|
|
3118
|
+
.dc-live-new, .dc-live-new > td { animation: none; }
|
|
3119
|
+
}
|
|
2940
3120
|
`;
|
|
2941
3121
|
|
|
2942
3122
|
let styleEl = null;
|
|
@@ -3542,7 +3722,7 @@
|
|
|
3542
3722
|
|
|
3543
3723
|
let netQueue = Promise.resolve();
|
|
3544
3724
|
|
|
3545
|
-
const siteFetchText = (url, options = {}, { background = false, auto = false } = {}) => {
|
|
3725
|
+
const siteFetchText = (url, options = {}, { background = false, auto = false, probe = false } = {}) => {
|
|
3546
3726
|
const run = async () => {
|
|
3547
3727
|
let state = readNetState();
|
|
3548
3728
|
|
|
@@ -3623,6 +3803,14 @@
|
|
|
3623
3803
|
|
|
3624
3804
|
let { response, text } = await fetchOnce('');
|
|
3625
3805
|
|
|
3806
|
+
if (probe) {
|
|
3807
|
+
if (!response.ok) {
|
|
3808
|
+
throw new Error(`HTTP ${response.status}`);
|
|
3809
|
+
}
|
|
3810
|
+
|
|
3811
|
+
return text;
|
|
3812
|
+
}
|
|
3813
|
+
|
|
3626
3814
|
if (looksBlockedResponse(response, text)) {
|
|
3627
3815
|
if (unreliableWindow(startedAt) || truncatedResponse(response, text)) {
|
|
3628
3816
|
throw new NetTransientError('unreliable');
|
|
@@ -5697,6 +5885,115 @@
|
|
|
5697
5885
|
const isDeferActive = () =>
|
|
5698
5886
|
settings.deferHeavyMedia === true || (settings.autoSaveData !== false && connectionSaveData());
|
|
5699
5887
|
|
|
5888
|
+
const mediaBoxes = new WeakMap();
|
|
5889
|
+
|
|
5890
|
+
const fitToolsToMedia = (mediaEl, tools) => {
|
|
5891
|
+
const parent = tools.parentElement;
|
|
5892
|
+
|
|
5893
|
+
let box = mediaBoxes.get(mediaEl) || tools._dcBox;
|
|
5894
|
+
|
|
5895
|
+
if (mediaBoxes.has(mediaEl)) {
|
|
5896
|
+
tools._dcBox = box;
|
|
5897
|
+
}
|
|
5898
|
+
|
|
5899
|
+
if (IS_MOBILE_SITE) {
|
|
5900
|
+
tools.style.setProperty('width', '100%');
|
|
5901
|
+
tools.style.removeProperty('margin-left');
|
|
5902
|
+
return;
|
|
5903
|
+
}
|
|
5904
|
+
|
|
5905
|
+
if (!box || !parent) {
|
|
5906
|
+
tools.style.removeProperty('width');
|
|
5907
|
+
tools.style.removeProperty('margin-left');
|
|
5908
|
+
return;
|
|
5909
|
+
}
|
|
5910
|
+
|
|
5911
|
+
const parentRect = parent.getBoundingClientRect();
|
|
5912
|
+
const room = parent.clientWidth || parentRect.width;
|
|
5913
|
+
const width = Math.min(box.width, room || box.width);
|
|
5914
|
+
let left = box.left - parentRect.left - (parseFloat(getComputedStyle(parent).paddingLeft) || 0);
|
|
5915
|
+
|
|
5916
|
+
if (!(left > 0) || left + width > room + 1) {
|
|
5917
|
+
left = 0;
|
|
5918
|
+
}
|
|
5919
|
+
|
|
5920
|
+
tools.style.setProperty('width', `${Math.round(width)}px`);
|
|
5921
|
+
tools.style.setProperty('margin-left', `${Math.round(left)}px`);
|
|
5922
|
+
};
|
|
5923
|
+
|
|
5924
|
+
const refitMediaTools = mediaEl => {
|
|
5925
|
+
const tools = mediaTools.get(mediaEl);
|
|
5926
|
+
const host = mediaHosts.get(mediaEl) || mediaEl;
|
|
5927
|
+
|
|
5928
|
+
if (!tools || !tools.isConnected) {
|
|
5929
|
+
return;
|
|
5930
|
+
}
|
|
5931
|
+
|
|
5932
|
+
if (!host.classList.contains('dc-media-hidden')) {
|
|
5933
|
+
const shown = mediaEl.getBoundingClientRect();
|
|
5934
|
+
|
|
5935
|
+
if (shown.width > 40) {
|
|
5936
|
+
mediaBoxes.set(mediaEl, { width: shown.width, left: shown.left });
|
|
5937
|
+
}
|
|
5938
|
+
}
|
|
5939
|
+
|
|
5940
|
+
fitToolsToMedia(mediaEl, tools);
|
|
5941
|
+
};
|
|
5942
|
+
|
|
5943
|
+
let refitTimer = null;
|
|
5944
|
+
|
|
5945
|
+
window.addEventListener('resize', () => {
|
|
5946
|
+
clearTimeout(refitTimer);
|
|
5947
|
+
refitTimer = setTimeout(() => {
|
|
5948
|
+
document.querySelectorAll('.dc-media-tools').forEach(tools => {
|
|
5949
|
+
const mediaEl = tools.previousElementSibling && tools.previousElementSibling.matches('img, video') ? tools.previousElementSibling : null;
|
|
5950
|
+
|
|
5951
|
+
if (mediaEl) {
|
|
5952
|
+
refitMediaTools(mediaEl);
|
|
5953
|
+
}
|
|
5954
|
+
});
|
|
5955
|
+
}, 200);
|
|
5956
|
+
}, { passive: true });
|
|
5957
|
+
|
|
5958
|
+
const hiddenImageComments = new Set();
|
|
5959
|
+
let imageCommentStyle = null;
|
|
5960
|
+
|
|
5961
|
+
const imageFileNo = (mediaEl, host) => {
|
|
5962
|
+
const own = mediaEl.getAttribute('data-fileno');
|
|
5963
|
+
|
|
5964
|
+
if (own) {
|
|
5965
|
+
return own;
|
|
5966
|
+
}
|
|
5967
|
+
|
|
5968
|
+
const holder = host.matches('[data-fileno]') ? host : host.querySelector('[data-fileno]');
|
|
5969
|
+
|
|
5970
|
+
return holder ? holder.getAttribute('data-fileno') : '';
|
|
5971
|
+
};
|
|
5972
|
+
|
|
5973
|
+
const setImageCommentHidden = (mediaEl, host, hidden) => {
|
|
5974
|
+
const fileNo = imageFileNo(mediaEl, host);
|
|
5975
|
+
|
|
5976
|
+
if (!/^\d+$/.test(fileNo) || hiddenImageComments.has(fileNo) === hidden) {
|
|
5977
|
+
return;
|
|
5978
|
+
}
|
|
5979
|
+
|
|
5980
|
+
if (hidden) {
|
|
5981
|
+
hiddenImageComments.add(fileNo);
|
|
5982
|
+
} else {
|
|
5983
|
+
hiddenImageComments.delete(fileNo);
|
|
5984
|
+
}
|
|
5985
|
+
|
|
5986
|
+
if (!imageCommentStyle || !imageCommentStyle.isConnected) {
|
|
5987
|
+
imageCommentStyle = document.createElement('style');
|
|
5988
|
+
imageCommentStyle.id = 'dc-img-comment-hide';
|
|
5989
|
+
(document.head || document.documentElement).appendChild(imageCommentStyle);
|
|
5990
|
+
}
|
|
5991
|
+
|
|
5992
|
+
imageCommentStyle.textContent = [...hiddenImageComments]
|
|
5993
|
+
.map(no => `#img_comment_div${no}, #img_comment_open_btn${no}, #img_comment_write_div${no}`)
|
|
5994
|
+
.join(',\n') + (hiddenImageComments.size ? ' { display: none !important; }' : '');
|
|
5995
|
+
};
|
|
5996
|
+
|
|
5700
5997
|
const readToolIds = tools => (tools.dataset.dcMediaIds || '').split(',').filter(Boolean);
|
|
5701
5998
|
|
|
5702
5999
|
const createTools = () => {
|
|
@@ -5796,7 +6093,14 @@
|
|
|
5796
6093
|
mediaEl.classList.remove('dc-media-hidden');
|
|
5797
6094
|
}
|
|
5798
6095
|
|
|
6096
|
+
const shown = mediaEl.getBoundingClientRect();
|
|
6097
|
+
|
|
6098
|
+
if (shown.width > 40 && !blocked && !deferred && !host.classList.contains('dc-media-hidden')) {
|
|
6099
|
+
mediaBoxes.set(mediaEl, { width: shown.width, left: shown.left });
|
|
6100
|
+
}
|
|
6101
|
+
|
|
5799
6102
|
host.classList.toggle('dc-media-hidden', blocked || deferred);
|
|
6103
|
+
setImageCommentHidden(mediaEl, host, blocked);
|
|
5800
6104
|
|
|
5801
6105
|
const tools = existing && existing.isConnected ? existing : createTools();
|
|
5802
6106
|
|
|
@@ -5824,6 +6128,12 @@
|
|
|
5824
6128
|
tools.querySelector('.dc-media-btn-load').hidden = !deferred;
|
|
5825
6129
|
|
|
5826
6130
|
mediaSignature.set(mediaEl, signature);
|
|
6131
|
+
fitToolsToMedia(mediaEl, tools);
|
|
6132
|
+
|
|
6133
|
+
if (!mediaBoxes.has(mediaEl) && !blocked && !deferred && !mediaEl.dataset.dcFitWatch) {
|
|
6134
|
+
mediaEl.dataset.dcFitWatch = '1';
|
|
6135
|
+
mediaEl.addEventListener(mediaEl.tagName === 'VIDEO' ? 'loadedmetadata' : 'load', () => refitMediaTools(mediaEl), { once: true });
|
|
6136
|
+
}
|
|
5827
6137
|
|
|
5828
6138
|
return tools;
|
|
5829
6139
|
};
|
|
@@ -6161,19 +6471,29 @@
|
|
|
6161
6471
|
}
|
|
6162
6472
|
|
|
6163
6473
|
let counts = null;
|
|
6474
|
+
let answered = false;
|
|
6164
6475
|
|
|
6165
6476
|
const $ = window.jQuery;
|
|
6166
6477
|
|
|
6167
6478
|
if ($ && $.ajax) {
|
|
6168
6479
|
counts = await new Promise(resolve => {
|
|
6169
6480
|
$.ajax({ url: '/ajax/userCnt', type: 'post', data: { user: code }, timeout: 6000 })
|
|
6170
|
-
.done(res =>
|
|
6481
|
+
.done(res => {
|
|
6482
|
+
answered = true;
|
|
6483
|
+
resolve(res && res.board ? { post: String(res.board), reply: String(res.comment || '0') } : null);
|
|
6484
|
+
})
|
|
6171
6485
|
.fail(() => resolve(null));
|
|
6172
6486
|
});
|
|
6173
6487
|
}
|
|
6174
6488
|
|
|
6489
|
+
if (answered) {
|
|
6490
|
+
gallogCountCache.set(code, counts);
|
|
6491
|
+
|
|
6492
|
+
return counts;
|
|
6493
|
+
}
|
|
6494
|
+
|
|
6175
6495
|
if (!counts) {
|
|
6176
|
-
const html = await siteFetchText(`https://m.dcinside.com/gallog/${encodeURIComponent(code)}`, { credentials: 'same-origin' });
|
|
6496
|
+
const html = await siteFetchText(`https://m.dcinside.com/gallog/${encodeURIComponent(code)}`, { credentials: 'same-origin' }, { probe: true });
|
|
6177
6497
|
const text = html.replace(/<[^>]+>/g, ' ');
|
|
6178
6498
|
const post = (text.match(/게시물\s*\(\s*([\d,]+)\s*\)/) || [])[1] || '';
|
|
6179
6499
|
const reply = (text.match(/댓글\s*\(\s*([\d,]+)\s*\)/) || [])[1] || '';
|
|
@@ -6207,10 +6527,11 @@
|
|
|
6207
6527
|
const ip = (keys.find(k => k.type === 'ip') || {}).value || '';
|
|
6208
6528
|
const target = code || ip;
|
|
6209
6529
|
const gallog = code ? `/gallog/${encodeURIComponent(code)}` : '';
|
|
6210
|
-
const search = mobileSearchUrl(nick);
|
|
6530
|
+
const search = authorSearchUrl(keys) || mobileSearchUrl(nick);
|
|
6211
6531
|
const me = (document.getElementById('user_id') || {}).value || '';
|
|
6212
6532
|
const isMe = !!code && code === me;
|
|
6213
|
-
const
|
|
6533
|
+
const memo = memoForKeys(keys);
|
|
6534
|
+
const watched = isWatchedKeys(keys);
|
|
6214
6535
|
const blockLabel = (type, value, label) => `${blockedUsers.includes(`${type}::${value}`) ? '차단 해제' : '차단'}(${label})`;
|
|
6215
6536
|
const row = (attrs, label) => `<li><a ${attrs}>${escapeHtml(label)}</a></li>`;
|
|
6216
6537
|
const menu = document.createElement('div');
|
|
@@ -6233,7 +6554,8 @@
|
|
|
6233
6554
|
'<ul class="lst">' +
|
|
6234
6555
|
(code ? row(`href="${escapeHtml(gallog)}"`, '갤로그') : '') +
|
|
6235
6556
|
(search ? row(`href="${escapeHtml(search)}"`, '작성글 검색') : '') +
|
|
6236
|
-
(!isMe
|
|
6557
|
+
(!isMe ? row('href="#" data-act="memo"', memo ? `📝 메모 · ${memo.text.length > 14 ? `${memo.text.slice(0, 14)}…` : memo.text}` : '📝 메모 남기기') : '') +
|
|
6558
|
+
(!isMe ? row('href="#" data-act="watch"', watched ? '⭐ 관심 사용자 해제' : '☆ 관심 사용자 (새 글 알림)') : '') +
|
|
6237
6559
|
(!isMe && nick ? row(`href="#" data-key="${escapeHtml(`nick::${nick}`)}"`, blockLabel('nick', nick, '닉네임')) : '') +
|
|
6238
6560
|
(!isMe && code ? row(`href="#" data-key="${escapeHtml(`id::${code}`)}"`, blockLabel('id', code, '식별 코드')) : '') +
|
|
6239
6561
|
(!isMe && !code && ip ? row(`href="#" data-key="${escapeHtml(`ip::${ip}`)}"`, blockLabel('ip', ip, 'IP')) : '') +
|
|
@@ -6251,7 +6573,14 @@
|
|
|
6251
6573
|
if (link.dataset.act === 'memo') {
|
|
6252
6574
|
event.preventDefault();
|
|
6253
6575
|
closePopover();
|
|
6254
|
-
|
|
6576
|
+
openMemoEditor(keys, nick || target);
|
|
6577
|
+
return;
|
|
6578
|
+
}
|
|
6579
|
+
|
|
6580
|
+
if (link.dataset.act === 'watch') {
|
|
6581
|
+
event.preventDefault();
|
|
6582
|
+
closePopover();
|
|
6583
|
+
toggleWatchUser(keys);
|
|
6255
6584
|
return;
|
|
6256
6585
|
}
|
|
6257
6586
|
|
|
@@ -6328,9 +6657,36 @@
|
|
|
6328
6657
|
);
|
|
6329
6658
|
})
|
|
6330
6659
|
.join('') +
|
|
6660
|
+
'<div class="dc-pop-extra">' +
|
|
6661
|
+
`<button type="button" class="dc-pop-mini" data-act="memo" title="${memoForKeys(keys) ? '메모 고치기' : '메모 남기기'}">📝 메모</button>` +
|
|
6662
|
+
`<button type="button" class="dc-pop-mini${isWatchedKeys(keys) ? ' on' : ''}" data-act="watch" title="${isWatchedKeys(keys) ? '관심 사용자 해제' : '관심 사용자 등록 (새 글 알림)'}">${isWatchedKeys(keys) ? '⭐ 관심' : '☆ 관심'}</button>` +
|
|
6663
|
+
(authorSearchUrl(keys) ? '<button type="button" class="dc-pop-mini" data-act="search">🔍 작성글</button>' : '') +
|
|
6664
|
+
'</div>' +
|
|
6331
6665
|
|
|
6332
6666
|
'<button type="button" class="dc-pop-site">⋯ 사이트 메뉴 (갤로그·작성글)</button>';
|
|
6333
6667
|
|
|
6668
|
+
popover.querySelector('.dc-pop-extra').addEventListener('click', event => {
|
|
6669
|
+
const act = event.target instanceof Element ? event.target.closest('[data-act]')?.dataset.act : '';
|
|
6670
|
+
|
|
6671
|
+
if (!act) {
|
|
6672
|
+
return;
|
|
6673
|
+
}
|
|
6674
|
+
|
|
6675
|
+
event.preventDefault();
|
|
6676
|
+
event.stopPropagation();
|
|
6677
|
+
closePopover();
|
|
6678
|
+
|
|
6679
|
+
const nickKey = keys.find(k => k.type === 'nick');
|
|
6680
|
+
|
|
6681
|
+
if (act === 'memo') {
|
|
6682
|
+
openMemoEditor(keys, nickKey ? nickKey.value : '');
|
|
6683
|
+
} else if (act === 'watch') {
|
|
6684
|
+
toggleWatchUser(keys);
|
|
6685
|
+
} else if (act === 'search') {
|
|
6686
|
+
location.href = authorSearchUrl(keys);
|
|
6687
|
+
}
|
|
6688
|
+
});
|
|
6689
|
+
|
|
6334
6690
|
popover.querySelector('.dc-pop-site').addEventListener('click', event => {
|
|
6335
6691
|
event.preventDefault();
|
|
6336
6692
|
event.stopPropagation();
|
|
@@ -7136,6 +7492,25 @@
|
|
|
7136
7492
|
hideEmptyReplyGroups(collapse);
|
|
7137
7493
|
});
|
|
7138
7494
|
|
|
7495
|
+
const restoreFullNick = (nickElement, fullNick) => {
|
|
7496
|
+
if (!fullNick || !nickElement || nickElement.dataset.dcFullNick) {
|
|
7497
|
+
return;
|
|
7498
|
+
}
|
|
7499
|
+
|
|
7500
|
+
const walker = document.createTreeWalker(nickElement, NodeFilter.SHOW_TEXT);
|
|
7501
|
+
|
|
7502
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
7503
|
+
const match = node.nodeValue.match(/^(\s*)(.+?)(\.\.|…)(\s*(?:\([^)]*\))?\s*)$/);
|
|
7504
|
+
|
|
7505
|
+
if (match && match[2].length < fullNick.length && fullNick.startsWith(match[2].trim())) {
|
|
7506
|
+
node.nodeValue = `${match[1]}${fullNick}${match[4]}`;
|
|
7507
|
+
nickElement.dataset.dcFullNick = 'true';
|
|
7508
|
+
nickElement.title = fullNick;
|
|
7509
|
+
break;
|
|
7510
|
+
}
|
|
7511
|
+
}
|
|
7512
|
+
};
|
|
7513
|
+
|
|
7139
7514
|
const processUserNode = (container, nickElement, source) => {
|
|
7140
7515
|
if (!container || !nickElement || container.dataset.dcProcessed) {
|
|
7141
7516
|
return;
|
|
@@ -7157,7 +7532,9 @@
|
|
|
7157
7532
|
|
|
7158
7533
|
const keys = buildBlockKeys(info);
|
|
7159
7534
|
|
|
7535
|
+
restoreFullNick(nickElement, info.nickname);
|
|
7160
7536
|
attachBlockTrigger(nickElement, keys);
|
|
7537
|
+
decorateUserNode(nickElement, keys);
|
|
7161
7538
|
|
|
7162
7539
|
const area = IS_MOBILE_SITE ? nickElement.closest('.ginfo-area') : null;
|
|
7163
7540
|
|
|
@@ -7414,6 +7791,7 @@
|
|
|
7414
7791
|
processMediaElements();
|
|
7415
7792
|
pinReadColors();
|
|
7416
7793
|
applyUserBlockState();
|
|
7794
|
+
applyAuthorFilter();
|
|
7417
7795
|
applyHeadTabVisibility();
|
|
7418
7796
|
bindHeadTabLongPress();
|
|
7419
7797
|
maintainListStream();
|
|
@@ -9866,6 +10244,7 @@
|
|
|
9866
10244
|
];
|
|
9867
10245
|
|
|
9868
10246
|
let blockSection = 'keywords';
|
|
10247
|
+
let userBlockType = 'nick';
|
|
9869
10248
|
let blockFilter = '';
|
|
9870
10249
|
|
|
9871
10250
|
const makeInputRow = (placeholder, buttonLabel, onSubmit, { maxLength = WORD_MAX_LENGTH } = {}) => {
|
|
@@ -10182,39 +10561,107 @@
|
|
|
10182
10561
|
},
|
|
10183
10562
|
|
|
10184
10563
|
users: area => {
|
|
10185
|
-
|
|
10564
|
+
const entries = blockedUsers.map(parseBlockKey);
|
|
10565
|
+
const count = type => entries.filter(entry => entry.type === type).length;
|
|
10566
|
+
|
|
10567
|
+
area.append(
|
|
10568
|
+
makeSectionTitle(`👤 유저 차단 (${blockedUsers.length})`),
|
|
10569
|
+
makeNote('기준마다 따로 모아 봅니다. 닉네임 = 같은 닉네임 모두(유동 포함), 식별코드 = 그 고닉 한 사람, IP = 그 유동 IP 대역. 닉네임을 눌러 차단하면 이곳에 쌓입니다.')
|
|
10570
|
+
);
|
|
10571
|
+
|
|
10572
|
+
const seg = h('div', 'dc-seg dc-word-scope');
|
|
10573
|
+
|
|
10574
|
+
Object.keys(BLOCK_TYPE_LABELS).forEach(type => {
|
|
10575
|
+
const btn = h('button', `dc-seg-btn${userBlockType === type ? ' active' : ''}`);
|
|
10576
|
+
|
|
10577
|
+
btn.type = 'button';
|
|
10578
|
+
btn.setAttribute('aria-pressed', String(userBlockType === type));
|
|
10579
|
+
btn.append(h('span', '', BLOCK_TYPE_LABELS[type]), h('span', 'dc-seg-count', String(count(type))));
|
|
10580
|
+
btn.addEventListener('click', () => {
|
|
10581
|
+
userBlockType = type;
|
|
10582
|
+
blockFilter = '';
|
|
10583
|
+
rerenderModal();
|
|
10584
|
+
});
|
|
10585
|
+
seg.appendChild(btn);
|
|
10586
|
+
});
|
|
10587
|
+
|
|
10588
|
+
area.appendChild(seg);
|
|
10589
|
+
area.appendChild(
|
|
10590
|
+
makeInputRow(
|
|
10591
|
+
userBlockType === 'ip' ? 'IP 앞자리 (예: 118.235)' : userBlockType === 'id' ? '식별코드 (갤로그 주소의 아이디)' : '닉네임',
|
|
10592
|
+
'차단',
|
|
10593
|
+
value => {
|
|
10594
|
+
const values = String(value)
|
|
10595
|
+
.split(/[,\n]/)
|
|
10596
|
+
.map(item => item.trim())
|
|
10597
|
+
.filter(Boolean)
|
|
10598
|
+
.filter(item => userBlockType !== 'ip' || /^\d{1,3}\.\d{1,3}$/.test(item));
|
|
10599
|
+
let added = 0;
|
|
10600
|
+
|
|
10601
|
+
values.forEach(item => {
|
|
10602
|
+
const key = `${userBlockType}::${item}`;
|
|
10603
|
+
|
|
10604
|
+
if (!blockedUsers.includes(key)) {
|
|
10605
|
+
blockedUsers = [...blockedUsers, key];
|
|
10606
|
+
added += 1;
|
|
10607
|
+
}
|
|
10608
|
+
});
|
|
10609
|
+
|
|
10610
|
+
if (added) {
|
|
10611
|
+
saveUsers();
|
|
10612
|
+
refreshAfterBlockChange();
|
|
10613
|
+
} else if (userBlockType === 'ip' && values.length === 0) {
|
|
10614
|
+
showToast('IP 는 앞 두 자리(예: 118.235) 형식으로 넣어 주세요');
|
|
10615
|
+
}
|
|
10616
|
+
|
|
10617
|
+
rerenderModal();
|
|
10618
|
+
},
|
|
10619
|
+
{ maxLength: 400 }
|
|
10620
|
+
)
|
|
10621
|
+
);
|
|
10186
10622
|
|
|
10187
|
-
const
|
|
10623
|
+
const list = entries.filter(entry => entry.type === userBlockType);
|
|
10624
|
+
const filterRow = makeFilterRow(list.length);
|
|
10188
10625
|
|
|
10189
10626
|
if (filterRow) {
|
|
10190
10627
|
area.appendChild(filterRow);
|
|
10191
10628
|
}
|
|
10192
10629
|
|
|
10193
|
-
if (!
|
|
10194
|
-
area.appendChild(makeEmpty(
|
|
10195
|
-
return;
|
|
10630
|
+
if (!list.length) {
|
|
10631
|
+
area.appendChild(makeEmpty(`${BLOCK_TYPE_LABELS[userBlockType]}(으)로 차단한 유저가 없습니다.`));
|
|
10196
10632
|
}
|
|
10197
10633
|
|
|
10198
|
-
|
|
10199
|
-
.map(parseBlockKey)
|
|
10200
|
-
.forEach(entry => {
|
|
10201
|
-
area.appendChild(
|
|
10202
|
-
makeRow({
|
|
10203
|
-
title: entry.value,
|
|
10204
|
-
meta: `${BLOCK_TYPE_LABELS[entry.type]} 기준`,
|
|
10205
|
-
actions: [{ label: '해제', className: 'on', onClick: () => { toggleUserBlockKey(entry.key); rerenderModal(); } }]
|
|
10206
|
-
})
|
|
10207
|
-
);
|
|
10208
|
-
});
|
|
10634
|
+
const memos = readMemos();
|
|
10209
10635
|
|
|
10210
|
-
|
|
10211
|
-
|
|
10212
|
-
|
|
10213
|
-
|
|
10214
|
-
|
|
10215
|
-
|
|
10216
|
-
|
|
10217
|
-
|
|
10636
|
+
list.forEach(entry => {
|
|
10637
|
+
const memo = (memos[entry.key] || {}).t;
|
|
10638
|
+
|
|
10639
|
+
area.appendChild(
|
|
10640
|
+
makeRow({
|
|
10641
|
+
title: entry.value,
|
|
10642
|
+
chip: BLOCK_TYPE_LABELS[entry.type],
|
|
10643
|
+
chipClass: BLOCK_TYPE_CLASS[entry.type],
|
|
10644
|
+
meta: memo ? `📝 ${memo}` : '',
|
|
10645
|
+
actions: [
|
|
10646
|
+
{ label: '메모', onClick: () => openMemoEditor([entry], entry.value) },
|
|
10647
|
+
{ label: '해제', className: 'on', onClick: () => { toggleUserBlockKey(entry.key); rerenderModal(); } }
|
|
10648
|
+
]
|
|
10649
|
+
})
|
|
10650
|
+
);
|
|
10651
|
+
});
|
|
10652
|
+
|
|
10653
|
+
applyListFilter(area);
|
|
10654
|
+
|
|
10655
|
+
if (list.length) {
|
|
10656
|
+
area.appendChild(
|
|
10657
|
+
makeConfirmButton(`${BLOCK_TYPE_LABELS[userBlockType]} 차단 전체 해제`, `${BLOCK_TYPE_LABELS[userBlockType]}(으)로 차단한 유저 ${list.length}명을 모두 해제할까요?`, () => {
|
|
10658
|
+
blockedUsers = blockedUsers.filter(key => parseBlockKey(key).type !== userBlockType);
|
|
10659
|
+
saveUsers();
|
|
10660
|
+
refreshAfterBlockChange();
|
|
10661
|
+
rerenderModal();
|
|
10662
|
+
})
|
|
10663
|
+
);
|
|
10664
|
+
}
|
|
10218
10665
|
},
|
|
10219
10666
|
|
|
10220
10667
|
media: area => {
|
|
@@ -10501,6 +10948,19 @@
|
|
|
10501
10948
|
off: '이 탭을 볼 때 말풍선으로만 알립니다.',
|
|
10502
10949
|
refresh: 'root'
|
|
10503
10950
|
},
|
|
10951
|
+
{
|
|
10952
|
+
group: 'alarm',
|
|
10953
|
+
key: 'watchAlerts',
|
|
10954
|
+
name: '관심 키워드·관심 사용자 새 글 알림',
|
|
10955
|
+
on: '새 글 자동 이어 붙이기로 들어온 글 중 관심 키워드가 제목에 있거나 관심 사용자가 쓴 글이면 알림을 띄웁니다. 관심 목록은 🔔 관심 탭에서 관리합니다.',
|
|
10956
|
+
off: '관심 글도 따로 알리지 않습니다 (목록에는 그대로 붙습니다).',
|
|
10957
|
+
status: on => {
|
|
10958
|
+
const watch = readWatch();
|
|
10959
|
+
|
|
10960
|
+
return on ? `관심 키워드 ${watch.words.length}개 · 관심 사용자 ${watch.users.length}명${settings.liveRefresh === false ? ' · 새 글 자동 이어 붙이기가 꺼져 있어 쉬는 중' : ''}` : null;
|
|
10961
|
+
},
|
|
10962
|
+
refresh: 'none'
|
|
10963
|
+
},
|
|
10504
10964
|
{
|
|
10505
10965
|
group: 'alarm',
|
|
10506
10966
|
key: 'alarmKeepPolling',
|
|
@@ -10579,6 +11039,29 @@
|
|
|
10579
11039
|
: null,
|
|
10580
11040
|
refresh: 'stream'
|
|
10581
11041
|
},
|
|
11042
|
+
{
|
|
11043
|
+
group: 'list',
|
|
11044
|
+
key: 'liveRefresh',
|
|
11045
|
+
name: '새 글 자동으로 이어 붙이기',
|
|
11046
|
+
on: '목록 1페이지를 보는 동안 새 글을 확인해 새로고침 없이 맨 위에 붙입니다. 내려서 읽는 중이면 읽던 자리를 그대로 두고 [↑ 새 글 N개] 만 띄웁니다. 화면을 보고 있을 때만, 10분 넘게 손대지 않으면 쉽니다.',
|
|
11047
|
+
off: '새 글을 보려면 직접 새로고침합니다.',
|
|
11048
|
+
status: on => (on ? liveStatusText() : null),
|
|
11049
|
+
refresh: 'live'
|
|
11050
|
+
},
|
|
11051
|
+
{
|
|
11052
|
+
group: 'list',
|
|
11053
|
+
key: 'liveRefreshSec',
|
|
11054
|
+
parent: 'liveRefresh',
|
|
11055
|
+
name: '새 글 확인 간격',
|
|
11056
|
+
on: '짧을수록 빨리 보이지만 요청이 늘어납니다. 디시 서버에 부담이 되지 않게 30초보다 짧게는 확인하지 않습니다.',
|
|
11057
|
+
choices: [
|
|
11058
|
+
{ value: 30, label: '30초' },
|
|
11059
|
+
{ value: 60, label: '1분' },
|
|
11060
|
+
{ value: 120, label: '2분' },
|
|
11061
|
+
{ value: 300, label: '5분' }
|
|
11062
|
+
],
|
|
11063
|
+
refresh: 'live'
|
|
11064
|
+
},
|
|
10582
11065
|
{
|
|
10583
11066
|
group: 'list',
|
|
10584
11067
|
key: 'hideReadMark',
|
|
@@ -10781,6 +11264,10 @@
|
|
|
10781
11264
|
processElements();
|
|
10782
11265
|
break;
|
|
10783
11266
|
|
|
11267
|
+
case 'live':
|
|
11268
|
+
scheduleLive();
|
|
11269
|
+
break;
|
|
11270
|
+
|
|
10784
11271
|
case 'theme':
|
|
10785
11272
|
coordinateOtherDarkScripts();
|
|
10786
11273
|
|
|
@@ -10809,7 +11296,7 @@
|
|
|
10809
11296
|
|
|
10810
11297
|
const BACKUP_APP = 'dcinside-enhancer';
|
|
10811
11298
|
const BACKUP_APP_OLD = ['dc-refinery', 'dc-optimizer'];
|
|
10812
|
-
const BACKUP_KEYS = ['users', 'images', 'keywords', 'titles', 'heads', 'settings', 'scraps'];
|
|
11299
|
+
const BACKUP_KEYS = ['users', 'images', 'keywords', 'titles', 'heads', 'settings', 'scraps', 'memos', 'watch'];
|
|
10813
11300
|
const USER_KEY_RE = /^(?:nick|ip|id)::.+/;
|
|
10814
11301
|
|
|
10815
11302
|
const exportData = () => ({
|
|
@@ -10823,6 +11310,8 @@
|
|
|
10823
11310
|
keywords: blockedKeywords,
|
|
10824
11311
|
titles: blockedTitles,
|
|
10825
11312
|
heads: hiddenHeadMap,
|
|
11313
|
+
memos: Object.fromEntries(Object.entries(readMemos()).filter(([, item]) => item.t)),
|
|
11314
|
+
watch: readWatch(),
|
|
10826
11315
|
settings
|
|
10827
11316
|
});
|
|
10828
11317
|
|
|
@@ -10880,12 +11369,36 @@
|
|
|
10880
11369
|
|
|
10881
11370
|
const importedScraps = Array.isArray(parsed.scraps) ? uniqueScraps(parsed.scraps.filter(isValidScrap).map(normalizeScrap)) : null;
|
|
10882
11371
|
|
|
10883
|
-
|
|
11372
|
+
let memos = null;
|
|
11373
|
+
|
|
11374
|
+
if (parsed.memos && typeof parsed.memos === 'object' && !Array.isArray(parsed.memos)) {
|
|
11375
|
+
memos = {};
|
|
11376
|
+
Object.entries(parsed.memos).forEach(([key, item]) => {
|
|
11377
|
+
const text = item && typeof item === 'object' ? String(item.t || '').trim().slice(0, 300) : '';
|
|
11378
|
+
|
|
11379
|
+
if (USER_KEY_RE.test(key) && text) {
|
|
11380
|
+
memos[key] = { t: text, at: Number(item.at) || Date.now() };
|
|
11381
|
+
}
|
|
11382
|
+
});
|
|
11383
|
+
}
|
|
11384
|
+
|
|
11385
|
+
let watch = null;
|
|
11386
|
+
|
|
11387
|
+
if (parsed.watch && typeof parsed.watch === 'object' && !Array.isArray(parsed.watch)) {
|
|
11388
|
+
watch = {
|
|
11389
|
+
users: Array.isArray(parsed.watch.users) ? [...new Set(parsed.watch.users.filter(key => typeof key === 'string' && USER_KEY_RE.test(key)))] : [],
|
|
11390
|
+
words: Array.isArray(parsed.watch.words) ? [...new Set(parsed.watch.words.map(word => normalizeWord(String(word || ''))).filter(word => word && word.length <= 60))] : []
|
|
11391
|
+
};
|
|
11392
|
+
}
|
|
11393
|
+
|
|
11394
|
+
return {
|
|
10884
11395
|
users,
|
|
10885
11396
|
images,
|
|
10886
11397
|
keywords,
|
|
10887
11398
|
titles,
|
|
10888
11399
|
heads,
|
|
11400
|
+
memos,
|
|
11401
|
+
watch,
|
|
10889
11402
|
scraps: importedScraps,
|
|
10890
11403
|
settings: importedSettings,
|
|
10891
11404
|
meta: {
|
|
@@ -10906,6 +11419,8 @@
|
|
|
10906
11419
|
if (data.heads) parts.push(`말머리 ${headCount}${headGalleries ? `(갤러리 ${headGalleries})` : ''}`);
|
|
10907
11420
|
if (data.images) parts.push(`이미지 ${data.images.length}`);
|
|
10908
11421
|
if (data.scraps) parts.push(`스크랩 ${data.scraps.length}`);
|
|
11422
|
+
if (data.memos && Object.keys(data.memos).length) parts.push(`메모 ${Object.keys(data.memos).length}`);
|
|
11423
|
+
if (data.watch && data.watch.users.length + data.watch.words.length) parts.push(`관심 ${data.watch.users.length + data.watch.words.length}`);
|
|
10909
11424
|
if (data.settings) parts.push('설정 포함');
|
|
10910
11425
|
|
|
10911
11426
|
return parts.join(' · ') || '비어 있음';
|
|
@@ -10968,6 +11483,29 @@
|
|
|
10968
11483
|
saveHiddenHeads();
|
|
10969
11484
|
}
|
|
10970
11485
|
|
|
11486
|
+
if (data.memos) {
|
|
11487
|
+
const now = Date.now();
|
|
11488
|
+
const current = readMemos();
|
|
11489
|
+
const next = merge ? { ...current } : {};
|
|
11490
|
+
|
|
11491
|
+
if (!merge) {
|
|
11492
|
+
Object.keys(current).forEach(key => {
|
|
11493
|
+
next[key] = { t: '', at: now };
|
|
11494
|
+
});
|
|
11495
|
+
}
|
|
11496
|
+
|
|
11497
|
+
Object.entries(data.memos).forEach(([key, item]) => {
|
|
11498
|
+
next[key] = { t: item.t, at: Math.max(now, item.at) };
|
|
11499
|
+
});
|
|
11500
|
+
writeMemos(next);
|
|
11501
|
+
}
|
|
11502
|
+
|
|
11503
|
+
if (data.watch) {
|
|
11504
|
+
const current = readWatch();
|
|
11505
|
+
|
|
11506
|
+
writeWatch(merge ? { users: [...current.users, ...data.watch.users], words: [...current.words, ...data.watch.words] } : data.watch);
|
|
11507
|
+
}
|
|
11508
|
+
|
|
10971
11509
|
if (data.settings && !merge) {
|
|
10972
11510
|
settings = { ...settings, ...data.settings };
|
|
10973
11511
|
saveSettings();
|
|
@@ -11845,6 +12383,8 @@
|
|
|
11845
12383
|
{ key: 'scraps', label: '스크랩', fields: ['scraps'] },
|
|
11846
12384
|
{ key: 'recent', label: '최근 갤러리', fields: ['recent', 'recentOrder'] },
|
|
11847
12385
|
{ key: 'search', label: '검색 기록', fields: ['search'] },
|
|
12386
|
+
{ key: 'memos', label: '메모', fields: ['memos'] },
|
|
12387
|
+
{ key: 'watch', label: '관심 알림', fields: ['watch'] },
|
|
11848
12388
|
{ key: 'settings', label: '설정', fields: ['settings'] }
|
|
11849
12389
|
];
|
|
11850
12390
|
|
|
@@ -12027,7 +12567,9 @@
|
|
|
12027
12567
|
recent: collectRecentForSync(),
|
|
12028
12568
|
recentOrder: collectRecentOrderForSync(),
|
|
12029
12569
|
search: collectSearchForSync(),
|
|
12030
|
-
hits: collectHitsForSync()
|
|
12570
|
+
hits: collectHitsForSync(),
|
|
12571
|
+
memos: collectMemosForSync(),
|
|
12572
|
+
watch: readWatch()
|
|
12031
12573
|
});
|
|
12032
12574
|
|
|
12033
12575
|
const skippedFields = skip => SYNC_PARTS.filter(part => skip.includes(part.key)).flatMap(part => part.fields);
|
|
@@ -12137,7 +12679,9 @@
|
|
|
12137
12679
|
recent: mergeRecent(local.recent, base && base.recent, remote && remote.recent),
|
|
12138
12680
|
recentOrder: mergeRecentOrder(local.recentOrder, remote && remote.recentOrder),
|
|
12139
12681
|
search: mergeSearchHistory(local.search, base && base.search, remote && remote.search),
|
|
12140
|
-
hits: mergeBlockHits(local.hits, remote && remote.hits)
|
|
12682
|
+
hits: mergeBlockHits(local.hits, remote && remote.hits),
|
|
12683
|
+
memos: mergeMemos(local.memos, remote && remote.memos),
|
|
12684
|
+
watch: mergeWatch(local.watch, base && base.watch, remote && remote.watch)
|
|
12141
12685
|
};
|
|
12142
12686
|
|
|
12143
12687
|
skippedFields(skip).forEach(field => {
|
|
@@ -12173,6 +12717,12 @@
|
|
|
12173
12717
|
}
|
|
12174
12718
|
if (after.heads) diff('말머리', flatHeads(before.heads), flatHeads(after.heads));
|
|
12175
12719
|
if (after.scraps) diff('스크랩', (before.scraps || []).map(scrapKeyOf), after.scraps.map(scrapKeyOf));
|
|
12720
|
+
if (after.memos) {
|
|
12721
|
+
const memoText = memos => Object.entries(memos || {}).filter(([, item]) => item && item.t).map(([key, item]) => `${key}\u0000${item.t}`);
|
|
12722
|
+
|
|
12723
|
+
diff('메모', memoText(before.memos), memoText(after.memos));
|
|
12724
|
+
}
|
|
12725
|
+
if (after.watch) diff('관심', [...((before.watch || {}).users || []), ...((before.watch || {}).words || []).map(w => `w:${w}`)], [...(after.watch.users || []), ...(after.watch.words || []).map(w => `w:${w}`)]);
|
|
12176
12726
|
|
|
12177
12727
|
if (after.settings) {
|
|
12178
12728
|
const changed = Object.keys(pickKnownSettings(after.settings)).filter(key => JSON.stringify(after.settings[key]) !== JSON.stringify((before.settings || {})[key])).length;
|
|
@@ -12194,6 +12744,8 @@
|
|
|
12194
12744
|
delete data.recentOrder;
|
|
12195
12745
|
delete data.search;
|
|
12196
12746
|
delete data.hits;
|
|
12747
|
+
delete data.memos;
|
|
12748
|
+
delete data.watch;
|
|
12197
12749
|
|
|
12198
12750
|
return JSON.stringify(data);
|
|
12199
12751
|
};
|
|
@@ -12203,6 +12755,8 @@
|
|
|
12203
12755
|
const recentChanged = data && 'recent' in data ? applyRecentFromSync(data.recent) : false;
|
|
12204
12756
|
const searchChanged = data && 'search' in data ? applySearchFromSync(data.search) : false;
|
|
12205
12757
|
const hitsChanged = data && 'hits' in data ? applyHitsFromSync(data.hits) : false;
|
|
12758
|
+
const memosChanged = data && 'memos' in data ? applyMemosFromSync(data.memos) : false;
|
|
12759
|
+
const watchChanged = data && 'watch' in data ? applyWatchFromSync(data.watch) : false;
|
|
12206
12760
|
const clean = sanitizeBackup({ app: BACKUP_APP, ...data });
|
|
12207
12761
|
const before = blockSyncData();
|
|
12208
12762
|
|
|
@@ -12257,7 +12811,7 @@
|
|
|
12257
12811
|
refreshAfterBlockChange();
|
|
12258
12812
|
}
|
|
12259
12813
|
|
|
12260
|
-
return changed || recentChanged || orderChanged || searchChanged || hitsChanged;
|
|
12814
|
+
return changed || recentChanged || orderChanged || searchChanged || hitsChanged || memosChanged || watchChanged;
|
|
12261
12815
|
};
|
|
12262
12816
|
|
|
12263
12817
|
const saveSyncUndo = (before, incoming, summary, from) => {
|
|
@@ -12523,6 +13077,7 @@
|
|
|
12523
13077
|
|
|
12524
13078
|
const runId = ++syncRunId;
|
|
12525
13079
|
const epoch = syncEpoch;
|
|
13080
|
+
const startedAt = syncRunSince;
|
|
12526
13081
|
|
|
12527
13082
|
const key = readSyncKey();
|
|
12528
13083
|
const conf = readSyncConf();
|
|
@@ -12654,7 +13209,9 @@
|
|
|
12654
13209
|
} catch (error) {
|
|
12655
13210
|
syncPending = true;
|
|
12656
13211
|
|
|
12657
|
-
|
|
13212
|
+
const interrupted = netLeavingRecently() || (netResumedAt > 0 && netResumedAt >= startedAt) || (netHiddenAt > 0 && netHiddenAt >= startedAt);
|
|
13213
|
+
|
|
13214
|
+
if (runId !== syncRunId || (error && error.quiet) || interrupted || pageLeaving || leaving || epoch !== syncEpoch || document.visibilityState === 'hidden') {
|
|
12658
13215
|
writeSyncRt({ dirty: true });
|
|
12659
13216
|
|
|
12660
13217
|
if (runId === syncRunId && !pageLeaving && document.visibilityState !== 'hidden') {
|
|
@@ -12748,7 +13305,7 @@
|
|
|
12748
13305
|
rerenderModal();
|
|
12749
13306
|
};
|
|
12750
13307
|
|
|
12751
|
-
const SYNCED_STORAGE = [STORAGE.users, STORAGE.media, STORAGE.keywords, STORAGE.titles, STORAGE.heads, STORAGE.scraps, STORAGE.settings];
|
|
13308
|
+
const SYNCED_STORAGE = [STORAGE.users, STORAGE.media, STORAGE.keywords, STORAGE.titles, STORAGE.heads, STORAGE.scraps, STORAGE.settings, STORAGE.memos, STORAGE.watch];
|
|
12752
13309
|
|
|
12753
13310
|
storageWriteListeners.push(key => {
|
|
12754
13311
|
if (!syncApplying && SYNCED_STORAGE.includes(key) && syncReady()) {
|
|
@@ -15832,6 +16389,869 @@
|
|
|
15832
16389
|
return true;
|
|
15833
16390
|
};
|
|
15834
16391
|
|
|
16392
|
+
const MEMO_MAX = 300;
|
|
16393
|
+
const MEMO_TOMBSTONE_KEEP = 90 * 24 * 60 * 60 * 1000;
|
|
16394
|
+
|
|
16395
|
+
let memoCache = null;
|
|
16396
|
+
let watchCache = null;
|
|
16397
|
+
|
|
16398
|
+
window.addEventListener('storage', event => {
|
|
16399
|
+
if (!event.key || event.key === STORAGE.memos || event.key === STORAGE.watch) {
|
|
16400
|
+
memoCache = null;
|
|
16401
|
+
watchCache = null;
|
|
16402
|
+
refreshUserDecorations();
|
|
16403
|
+
}
|
|
16404
|
+
});
|
|
16405
|
+
|
|
16406
|
+
const rawStored = key => {
|
|
16407
|
+
try {
|
|
16408
|
+
return localStorage.getItem(key) || '';
|
|
16409
|
+
} catch {
|
|
16410
|
+
return '';
|
|
16411
|
+
}
|
|
16412
|
+
};
|
|
16413
|
+
|
|
16414
|
+
const readMemos = () => {
|
|
16415
|
+
const rawText = rawStored(STORAGE.memos);
|
|
16416
|
+
|
|
16417
|
+
if (memoCache && memoCache.raw === rawText) {
|
|
16418
|
+
return { ...memoCache.data };
|
|
16419
|
+
}
|
|
16420
|
+
|
|
16421
|
+
const raw = readObject(STORAGE.memos);
|
|
16422
|
+
const out = {};
|
|
16423
|
+
|
|
16424
|
+
Object.entries(raw || {}).forEach(([key, item]) => {
|
|
16425
|
+
if (/^(?:nick|ip|id)::.+/.test(key) && item && typeof item === 'object') {
|
|
16426
|
+
out[key] = { t: String(item.t || '').slice(0, MEMO_MAX), at: Number(item.at) || 0 };
|
|
16427
|
+
}
|
|
16428
|
+
});
|
|
16429
|
+
|
|
16430
|
+
memoCache = { raw: rawText, data: out };
|
|
16431
|
+
|
|
16432
|
+
return { ...out };
|
|
16433
|
+
};
|
|
16434
|
+
|
|
16435
|
+
const writeMemos = memos => {
|
|
16436
|
+
const now = Date.now();
|
|
16437
|
+
const out = {};
|
|
16438
|
+
|
|
16439
|
+
Object.entries(memos).forEach(([key, item]) => {
|
|
16440
|
+
if (item.t || now - item.at < MEMO_TOMBSTONE_KEEP) {
|
|
16441
|
+
out[key] = item;
|
|
16442
|
+
}
|
|
16443
|
+
});
|
|
16444
|
+
|
|
16445
|
+
memoCache = null;
|
|
16446
|
+
writeJson(STORAGE.memos, out);
|
|
16447
|
+
};
|
|
16448
|
+
|
|
16449
|
+
const memoCount = () => Object.values(readMemos()).filter(item => item.t).length;
|
|
16450
|
+
|
|
16451
|
+
const memoForKeys = keys => {
|
|
16452
|
+
const memos = readMemos();
|
|
16453
|
+
const order = ['id', 'ip', 'nick'];
|
|
16454
|
+
|
|
16455
|
+
for (const type of order) {
|
|
16456
|
+
const entry = keys.find(k => k.type === type);
|
|
16457
|
+
|
|
16458
|
+
if (entry && memos[entry.key] && memos[entry.key].t) {
|
|
16459
|
+
return { key: entry.key, text: memos[entry.key].t };
|
|
16460
|
+
}
|
|
16461
|
+
}
|
|
16462
|
+
|
|
16463
|
+
return null;
|
|
16464
|
+
};
|
|
16465
|
+
|
|
16466
|
+
const setMemo = (key, text) => {
|
|
16467
|
+
const memos = readMemos();
|
|
16468
|
+
|
|
16469
|
+
memos[key] = { t: String(text || '').trim().slice(0, MEMO_MAX), at: Date.now() };
|
|
16470
|
+
writeMemos(memos);
|
|
16471
|
+
refreshUserDecorations();
|
|
16472
|
+
};
|
|
16473
|
+
|
|
16474
|
+
const primaryUserKey = keys => (keys.find(k => k.type === 'id') || keys.find(k => k.type === 'ip') || keys[0] || {}).key || '';
|
|
16475
|
+
|
|
16476
|
+
const describeUserKey = key => {
|
|
16477
|
+
const entry = parseBlockKey(key);
|
|
16478
|
+
|
|
16479
|
+
return `${BLOCK_TYPE_LABELS[entry.type] || ''} ${entry.value}`.trim();
|
|
16480
|
+
};
|
|
16481
|
+
|
|
16482
|
+
const openMemoEditor = (keys, title) => {
|
|
16483
|
+
const key = primaryUserKey(keys);
|
|
16484
|
+
|
|
16485
|
+
if (!key) {
|
|
16486
|
+
return;
|
|
16487
|
+
}
|
|
16488
|
+
|
|
16489
|
+
document.querySelector('.dc-memo-dialog')?.remove();
|
|
16490
|
+
|
|
16491
|
+
const current = (readMemos()[key] || {}).t || '';
|
|
16492
|
+
const wrap = document.createElement('div');
|
|
16493
|
+
|
|
16494
|
+
wrap.className = 'dc-ui dc-memo-dialog';
|
|
16495
|
+
wrap.setAttribute('role', 'dialog');
|
|
16496
|
+
wrap.setAttribute('aria-modal', 'true');
|
|
16497
|
+
wrap.innerHTML =
|
|
16498
|
+
'<div class="dc-memo-box">' +
|
|
16499
|
+
`<div class="dc-memo-head"><strong>📝 메모</strong><span>${escapeHtml(title || describeUserKey(key))}</span></div>` +
|
|
16500
|
+
`<textarea maxlength="${MEMO_MAX}" placeholder="이 이용자에 대한 메모 (나만 보임 · 개수 제한 없음)"></textarea>` +
|
|
16501
|
+
`<div class="dc-memo-sub">${escapeHtml(describeUserKey(key))} 기준 · 목록·댓글의 이름 옆에 보입니다</div>` +
|
|
16502
|
+
'<div class="dc-memo-actions">' +
|
|
16503
|
+
(current ? '<button type="button" data-act="del" class="danger">지우기</button>' : '') +
|
|
16504
|
+
'<span></span><button type="button" data-act="cancel">취소</button><button type="button" data-act="save" class="primary">저장</button>' +
|
|
16505
|
+
'</div></div>';
|
|
16506
|
+
|
|
16507
|
+
const textarea = wrap.querySelector('textarea');
|
|
16508
|
+
|
|
16509
|
+
textarea.value = current;
|
|
16510
|
+
|
|
16511
|
+
const close = () => wrap.remove();
|
|
16512
|
+
|
|
16513
|
+
wrap.addEventListener('click', event => {
|
|
16514
|
+
const act = event.target instanceof Element ? event.target.closest('[data-act]')?.dataset.act : '';
|
|
16515
|
+
|
|
16516
|
+
if (event.target === wrap || act === 'cancel') {
|
|
16517
|
+
close();
|
|
16518
|
+
} else if (act === 'save') {
|
|
16519
|
+
setMemo(key, textarea.value);
|
|
16520
|
+
close();
|
|
16521
|
+
showToast(textarea.value.trim() ? '📝 메모를 저장했습니다' : '📝 메모를 지웠습니다');
|
|
16522
|
+
} else if (act === 'del') {
|
|
16523
|
+
setMemo(key, '');
|
|
16524
|
+
close();
|
|
16525
|
+
showToast('📝 메모를 지웠습니다');
|
|
16526
|
+
}
|
|
16527
|
+
});
|
|
16528
|
+
|
|
16529
|
+
textarea.addEventListener('keydown', event => {
|
|
16530
|
+
if (event.key === 'Escape') {
|
|
16531
|
+
close();
|
|
16532
|
+
}
|
|
16533
|
+
});
|
|
16534
|
+
|
|
16535
|
+
document.body.appendChild(wrap);
|
|
16536
|
+
setTimeout(() => textarea.focus(), 30);
|
|
16537
|
+
};
|
|
16538
|
+
|
|
16539
|
+
const WATCH_WORD_MAX = 60;
|
|
16540
|
+
|
|
16541
|
+
const readWatch = () => {
|
|
16542
|
+
const rawText = rawStored(STORAGE.watch);
|
|
16543
|
+
|
|
16544
|
+
if (watchCache && watchCache.raw === rawText) {
|
|
16545
|
+
return { users: [...watchCache.users], words: [...watchCache.words] };
|
|
16546
|
+
}
|
|
16547
|
+
|
|
16548
|
+
const raw = readObject(STORAGE.watch);
|
|
16549
|
+
const users = Array.isArray(raw.users) ? raw.users.filter(key => typeof key === 'string' && /^(?:nick|ip|id)::.+/.test(key)) : [];
|
|
16550
|
+
const words = Array.isArray(raw.words)
|
|
16551
|
+
? raw.words.map(word => normalizeWord(String(word || ''))).filter(word => word && word.length <= WATCH_WORD_MAX)
|
|
16552
|
+
: [];
|
|
16553
|
+
|
|
16554
|
+
watchCache = { raw: rawText, users: [...new Set(users)], words: [...new Set(words)] };
|
|
16555
|
+
|
|
16556
|
+
return { users: [...watchCache.users], words: [...watchCache.words] };
|
|
16557
|
+
};
|
|
16558
|
+
|
|
16559
|
+
const writeWatch = watch => {
|
|
16560
|
+
watchCache = null;
|
|
16561
|
+
writeJson(STORAGE.watch, { users: [...new Set(watch.users)], words: [...new Set(watch.words)] });
|
|
16562
|
+
refreshUserDecorations();
|
|
16563
|
+
};
|
|
16564
|
+
|
|
16565
|
+
const isWatchedKeys = keys => {
|
|
16566
|
+
const watched = readWatch().users;
|
|
16567
|
+
|
|
16568
|
+
return keys.some(k => watched.includes(k.key));
|
|
16569
|
+
};
|
|
16570
|
+
|
|
16571
|
+
const toggleWatchUser = keys => {
|
|
16572
|
+
const key = primaryUserKey(keys);
|
|
16573
|
+
const watch = readWatch();
|
|
16574
|
+
const on = keys.some(k => watch.users.includes(k.key));
|
|
16575
|
+
|
|
16576
|
+
watch.users = on ? watch.users.filter(item => !keys.some(k => k.key === item)) : [...watch.users, key];
|
|
16577
|
+
writeWatch(watch);
|
|
16578
|
+
showToast(on ? '☆ 관심 사용자에서 뺐습니다' : `⭐ 관심 사용자로 등록했습니다 · 새 글이 올라오면 알려 드립니다`);
|
|
16579
|
+
};
|
|
16580
|
+
|
|
16581
|
+
const addWatchWords = value => {
|
|
16582
|
+
const watch = readWatch();
|
|
16583
|
+
const words = String(value || '')
|
|
16584
|
+
.split(/[,\n]/)
|
|
16585
|
+
.map(normalizeWord)
|
|
16586
|
+
.filter(word => word && word.length <= WATCH_WORD_MAX && !watch.words.includes(word));
|
|
16587
|
+
|
|
16588
|
+
if (words.length) {
|
|
16589
|
+
watch.words = [...watch.words, ...words];
|
|
16590
|
+
writeWatch(watch);
|
|
16591
|
+
}
|
|
16592
|
+
|
|
16593
|
+
return words.length;
|
|
16594
|
+
};
|
|
16595
|
+
|
|
16596
|
+
const decoratedNodes = new Set();
|
|
16597
|
+
|
|
16598
|
+
const decorateUserNode = (nickElement, keys) => {
|
|
16599
|
+
if (!nickElement || !keys || !keys.length) {
|
|
16600
|
+
return;
|
|
16601
|
+
}
|
|
16602
|
+
|
|
16603
|
+
nickElement._dcDecorKeys = keys;
|
|
16604
|
+
decoratedNodes.add(nickElement);
|
|
16605
|
+
|
|
16606
|
+
const memo = memoForKeys(keys);
|
|
16607
|
+
const watched = isWatchedKeys(keys);
|
|
16608
|
+
let chip = nickElement._dcMemoChip;
|
|
16609
|
+
|
|
16610
|
+
nickElement.classList.toggle('dc-watch-user', watched);
|
|
16611
|
+
|
|
16612
|
+
if (!memo) {
|
|
16613
|
+
if (chip) {
|
|
16614
|
+
chip.remove();
|
|
16615
|
+
nickElement._dcMemoChip = null;
|
|
16616
|
+
}
|
|
16617
|
+
|
|
16618
|
+
return;
|
|
16619
|
+
}
|
|
16620
|
+
|
|
16621
|
+
if (!chip || !chip.isConnected) {
|
|
16622
|
+
chip = document.createElement('span');
|
|
16623
|
+
chip.className = 'dc-ui dc-memo-chip';
|
|
16624
|
+
|
|
16625
|
+
if (nickElement.tagName === 'LI') {
|
|
16626
|
+
nickElement.appendChild(chip);
|
|
16627
|
+
} else {
|
|
16628
|
+
nickElement.insertAdjacentElement('afterend', chip);
|
|
16629
|
+
}
|
|
16630
|
+
nickElement._dcMemoChip = chip;
|
|
16631
|
+
}
|
|
16632
|
+
|
|
16633
|
+
chip.textContent = `📝 ${memo.text.length > 18 ? `${memo.text.slice(0, 18)}…` : memo.text}`;
|
|
16634
|
+
chip.title = memo.text;
|
|
16635
|
+
};
|
|
16636
|
+
|
|
16637
|
+
const refreshUserDecorations = () =>
|
|
16638
|
+
withSelfMutation(() => {
|
|
16639
|
+
decoratedNodes.forEach(node => {
|
|
16640
|
+
if (!node.isConnected) {
|
|
16641
|
+
decoratedNodes.delete(node);
|
|
16642
|
+
return;
|
|
16643
|
+
}
|
|
16644
|
+
|
|
16645
|
+
decorateUserNode(node, node._dcDecorKeys);
|
|
16646
|
+
});
|
|
16647
|
+
});
|
|
16648
|
+
|
|
16649
|
+
const AUTHOR_HASH_RE = /(?:^#|&)dcx-author=([^&]+)/;
|
|
16650
|
+
|
|
16651
|
+
const authorSearchUrl = (keys, gallery = getGalleryId()) => {
|
|
16652
|
+
const nick = (keys.find(k => k.type === 'nick') || {}).value || '';
|
|
16653
|
+
const exact = keys.find(k => k.type === 'id') || keys.find(k => k.type === 'ip');
|
|
16654
|
+
|
|
16655
|
+
if (!gallery || gallery === HOME_GALLERY_KEY || !nick) {
|
|
16656
|
+
return '';
|
|
16657
|
+
}
|
|
16658
|
+
|
|
16659
|
+
let url;
|
|
16660
|
+
|
|
16661
|
+
if (IS_MOBILE_SITE) {
|
|
16662
|
+
const kind = (location.pathname.match(/^\/(board|mini|person)\//) || [])[1] || 'board';
|
|
16663
|
+
|
|
16664
|
+
url = `https://m.dcinside.com/${kind}/${encodeURIComponent(gallery)}?s_type=name&serval=${encodeURIComponent(nick)}`;
|
|
16665
|
+
} else {
|
|
16666
|
+
const prefix = (location.pathname.match(/^\/(mgallery|mini|person)\//) || [])[1];
|
|
16667
|
+
|
|
16668
|
+
url = `https://gall.dcinside.com/${prefix ? `${prefix}/` : ''}board/lists/?id=${encodeURIComponent(gallery)}&s_type=search_name&s_keyword=${encodeURIComponent(nick)}`;
|
|
16669
|
+
}
|
|
16670
|
+
|
|
16671
|
+
return exact ? `${url}#dcx-author=${encodeURIComponent(exact.key)}` : url;
|
|
16672
|
+
};
|
|
16673
|
+
|
|
16674
|
+
const readAuthorFilter = () => {
|
|
16675
|
+
const match = AUTHOR_HASH_RE.exec(location.hash);
|
|
16676
|
+
|
|
16677
|
+
if (!match) {
|
|
16678
|
+
return null;
|
|
16679
|
+
}
|
|
16680
|
+
|
|
16681
|
+
try {
|
|
16682
|
+
const key = decodeURIComponent(match[1]);
|
|
16683
|
+
|
|
16684
|
+
return /^(?:ip|id)::.+/.test(key) ? parseBlockKey(key) : null;
|
|
16685
|
+
} catch {
|
|
16686
|
+
return null;
|
|
16687
|
+
}
|
|
16688
|
+
};
|
|
16689
|
+
|
|
16690
|
+
let authorFilterOff = false;
|
|
16691
|
+
|
|
16692
|
+
const rowMatchesAuthor = (li, filter) => {
|
|
16693
|
+
const holder = li.dataset.dcProcessed ? li : li.querySelector('[data-dc-processed]');
|
|
16694
|
+
|
|
16695
|
+
if (!holder) {
|
|
16696
|
+
return null;
|
|
16697
|
+
}
|
|
16698
|
+
|
|
16699
|
+
return filter.type === 'id' ? holder.dataset.dcCode === filter.value : !holder.dataset.dcCode && holder.dataset.dcIp === filter.value;
|
|
16700
|
+
};
|
|
16701
|
+
|
|
16702
|
+
const applyAuthorFilter = () => {
|
|
16703
|
+
const filter = readAuthorFilter();
|
|
16704
|
+
const listEl = filter ? pickListElement(document) : null;
|
|
16705
|
+
|
|
16706
|
+
if (!listEl) {
|
|
16707
|
+
return;
|
|
16708
|
+
}
|
|
16709
|
+
|
|
16710
|
+
let hidden = 0;
|
|
16711
|
+
|
|
16712
|
+
withSelfMutation(() => {
|
|
16713
|
+
realListItems(listEl).forEach(li => {
|
|
16714
|
+
const match = rowMatchesAuthor(li, filter);
|
|
16715
|
+
const hide = !authorFilterOff && match === false;
|
|
16716
|
+
|
|
16717
|
+
li.classList.toggle('dc-author-other', hide);
|
|
16718
|
+
hidden += match === false ? 1 : 0;
|
|
16719
|
+
});
|
|
16720
|
+
|
|
16721
|
+
let bar = document.getElementById('dc-author-bar');
|
|
16722
|
+
|
|
16723
|
+
if (!bar) {
|
|
16724
|
+
bar = document.createElement('div');
|
|
16725
|
+
bar.id = 'dc-author-bar';
|
|
16726
|
+
bar.className = 'dc-ui dc-author-bar';
|
|
16727
|
+
streamAnchorOf(listEl).insertAdjacentElement('beforebegin', bar);
|
|
16728
|
+
bar.addEventListener('click', event => {
|
|
16729
|
+
if (event.target instanceof Element && event.target.closest('button')) {
|
|
16730
|
+
authorFilterOff = !authorFilterOff;
|
|
16731
|
+
applyAuthorFilter();
|
|
16732
|
+
}
|
|
16733
|
+
});
|
|
16734
|
+
}
|
|
16735
|
+
|
|
16736
|
+
const label = `${BLOCK_TYPE_LABELS[filter.type]} ${filter.value}`;
|
|
16737
|
+
|
|
16738
|
+
bar.innerHTML = authorFilterOff
|
|
16739
|
+
? `<span>🔍 같은 닉네임의 글을 모두 보는 중</span><button type="button">${escapeHtml(label)} 글만</button>`
|
|
16740
|
+
: `<span>🔍 <b>${escapeHtml(label)}</b> 글만 보는 중${hidden ? ` · 같은 닉네임의 다른 사람 글 ${hidden}개 숨김` : ''}</span>${hidden ? '<button type="button">모두 보기</button>' : ''}`;
|
|
16741
|
+
});
|
|
16742
|
+
};
|
|
16743
|
+
|
|
16744
|
+
const LIVE_MIN_SEC = 30;
|
|
16745
|
+
const LIVE_IDLE_STOP = 10 * 60 * 1000;
|
|
16746
|
+
const LIVE_INSERT_MAX = 30;
|
|
16747
|
+
|
|
16748
|
+
let liveTimer = null;
|
|
16749
|
+
let liveBusy = false;
|
|
16750
|
+
let liveLastCheck = 0;
|
|
16751
|
+
let liveLastFound = 0;
|
|
16752
|
+
let liveInteractAt = 0;
|
|
16753
|
+
let livePill = null;
|
|
16754
|
+
let livePending = 0;
|
|
16755
|
+
|
|
16756
|
+
const liveIntervalMs = () => Math.max(LIVE_MIN_SEC, Number(settings.liveRefreshSec) || 60) * 1000;
|
|
16757
|
+
|
|
16758
|
+
const isLiveListPage = () => {
|
|
16759
|
+
if (settings.liveRefresh === false || isPostViewPage() || readAuthorFilter()) {
|
|
16760
|
+
return false;
|
|
16761
|
+
}
|
|
16762
|
+
|
|
16763
|
+
if (!/^\/(?:mgallery\/)?(?:board|mini|person)\//.test(location.pathname)) {
|
|
16764
|
+
return false;
|
|
16765
|
+
}
|
|
16766
|
+
|
|
16767
|
+
try {
|
|
16768
|
+
const params = new URL(location.href).searchParams;
|
|
16769
|
+
|
|
16770
|
+
if (['s_type', 'serval', 's_keyword', 'search_head'].some(name => params.get(name))) {
|
|
16771
|
+
return false;
|
|
16772
|
+
}
|
|
16773
|
+
} catch {
|
|
16774
|
+
return false;
|
|
16775
|
+
}
|
|
16776
|
+
|
|
16777
|
+
return currentPageNumber() === 1 && !!pickListElement(document);
|
|
16778
|
+
};
|
|
16779
|
+
|
|
16780
|
+
const topPostNumber = listEl =>
|
|
16781
|
+
realListItems(listEl).reduce((max, li) => Math.max(max, postNumber(li.dataset.dcPostKey || listItemKey(li))), 0);
|
|
16782
|
+
|
|
16783
|
+
const hideLivePill = () => {
|
|
16784
|
+
if (livePill) {
|
|
16785
|
+
livePill.classList.remove('show');
|
|
16786
|
+
}
|
|
16787
|
+
|
|
16788
|
+
livePending = 0;
|
|
16789
|
+
};
|
|
16790
|
+
|
|
16791
|
+
const showLivePill = (count, scrolled) => {
|
|
16792
|
+
if (!livePill) {
|
|
16793
|
+
livePill = document.createElement('button');
|
|
16794
|
+
livePill.type = 'button';
|
|
16795
|
+
livePill.className = 'dc-ui dc-live-pill';
|
|
16796
|
+
livePill.addEventListener('click', () => {
|
|
16797
|
+
hideLivePill();
|
|
16798
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
16799
|
+
});
|
|
16800
|
+
document.body.appendChild(livePill);
|
|
16801
|
+
}
|
|
16802
|
+
|
|
16803
|
+
livePending = scrolled ? livePending + count : count;
|
|
16804
|
+
livePill.textContent = scrolled ? `↑ 새 글 ${livePending}개` : `✨ 새 글 ${count}개`;
|
|
16805
|
+
livePill.classList.add('show');
|
|
16806
|
+
clearTimeout(livePill._timer);
|
|
16807
|
+
|
|
16808
|
+
if (!scrolled) {
|
|
16809
|
+
livePill._timer = setTimeout(hideLivePill, 2600);
|
|
16810
|
+
}
|
|
16811
|
+
};
|
|
16812
|
+
|
|
16813
|
+
const rowWriterKeys = li => {
|
|
16814
|
+
const holder = li.dataset.dcProcessed ? li : li.querySelector('[data-dc-processed]');
|
|
16815
|
+
|
|
16816
|
+
return holder
|
|
16817
|
+
? buildBlockKeys({ nickname: holder.dataset.dcNick || '', ip: holder.dataset.dcIp || '', code: holder.dataset.dcCode || '' })
|
|
16818
|
+
: [];
|
|
16819
|
+
};
|
|
16820
|
+
|
|
16821
|
+
const rowTitle = li => cleanText((li.querySelector('.subjectin, .gall_tit a:not(.reply_numbox)') || {}).textContent || '');
|
|
16822
|
+
|
|
16823
|
+
const rowIsShown = li =>
|
|
16824
|
+
li.isConnected &&
|
|
16825
|
+
!li.classList.contains('dc-user-blocked') &&
|
|
16826
|
+
!li.classList.contains('dc-head-hidden') &&
|
|
16827
|
+
!li.classList.contains('dc-author-other') &&
|
|
16828
|
+
getComputedStyle(li).display !== 'none';
|
|
16829
|
+
|
|
16830
|
+
const notifyWatchHits = rows => {
|
|
16831
|
+
if (settings.watchAlerts === false || !rows.length) {
|
|
16832
|
+
return;
|
|
16833
|
+
}
|
|
16834
|
+
|
|
16835
|
+
const watch = readWatch();
|
|
16836
|
+
|
|
16837
|
+
if (!watch.users.length && !watch.words.length) {
|
|
16838
|
+
return;
|
|
16839
|
+
}
|
|
16840
|
+
|
|
16841
|
+
const gallery = getGalleryId();
|
|
16842
|
+
const seen = readObject(STORAGE.liveSeen);
|
|
16843
|
+
const seenNo = Number(seen[gallery]) || 0;
|
|
16844
|
+
let maxNo = seenNo;
|
|
16845
|
+
const hits = [];
|
|
16846
|
+
|
|
16847
|
+
rows.forEach(li => {
|
|
16848
|
+
const no = postNumber(li.dataset.dcPostKey || listItemKey(li));
|
|
16849
|
+
|
|
16850
|
+
if (!no || no <= seenNo) {
|
|
16851
|
+
return;
|
|
16852
|
+
}
|
|
16853
|
+
|
|
16854
|
+
maxNo = Math.max(maxNo, no);
|
|
16855
|
+
|
|
16856
|
+
const title = rowTitle(li);
|
|
16857
|
+
const lower = title.toLowerCase();
|
|
16858
|
+
const word = watch.words.find(item => lower.includes(item.toLowerCase()));
|
|
16859
|
+
const keys = rowWriterKeys(li);
|
|
16860
|
+
const user = keys.find(k => watch.users.includes(k.key));
|
|
16861
|
+
|
|
16862
|
+
if (word || user) {
|
|
16863
|
+
li.classList.add('dc-live-watch');
|
|
16864
|
+
hits.push({ li, title, reason: word ? `키워드 '${word}'` : `관심 사용자 ${user.value}` });
|
|
16865
|
+
}
|
|
16866
|
+
});
|
|
16867
|
+
|
|
16868
|
+
if (maxNo > seenNo) {
|
|
16869
|
+
writeJson(STORAGE.liveSeen, { ...seen, [gallery]: maxNo });
|
|
16870
|
+
}
|
|
16871
|
+
|
|
16872
|
+
if (!hits.length) {
|
|
16873
|
+
return;
|
|
16874
|
+
}
|
|
16875
|
+
|
|
16876
|
+
const first = hits[0];
|
|
16877
|
+
const link = first.li.querySelector(POST_LINK_SELECTOR);
|
|
16878
|
+
const more = hits.length > 1 ? ` 외 ${hits.length - 1}개` : '';
|
|
16879
|
+
|
|
16880
|
+
showToast(`🔔 ${first.reason} · ${first.title}${more}`, {
|
|
16881
|
+
actionLabel: '보기',
|
|
16882
|
+
onAction: () => {
|
|
16883
|
+
if (link) {
|
|
16884
|
+
link.click();
|
|
16885
|
+
}
|
|
16886
|
+
},
|
|
16887
|
+
duration: 7000
|
|
16888
|
+
});
|
|
16889
|
+
|
|
16890
|
+
if (canSystemNotify() && document.visibilityState === 'hidden') {
|
|
16891
|
+
try {
|
|
16892
|
+
const note = new window.Notification(`디시 관심 알림 · ${first.reason}`, { body: `${first.title}${more}`, tag: `dcx-watch-${gallery}` });
|
|
16893
|
+
|
|
16894
|
+
note.onclick = () => {
|
|
16895
|
+
window.focus();
|
|
16896
|
+
note.close();
|
|
16897
|
+
|
|
16898
|
+
if (link) {
|
|
16899
|
+
link.click();
|
|
16900
|
+
}
|
|
16901
|
+
};
|
|
16902
|
+
} catch {
|
|
16903
|
+
}
|
|
16904
|
+
}
|
|
16905
|
+
};
|
|
16906
|
+
|
|
16907
|
+
const checkLiveList = async () => {
|
|
16908
|
+
if (liveBusy || !isLiveListPage() || document.visibilityState === 'hidden' || netPausedUntil()) {
|
|
16909
|
+
return;
|
|
16910
|
+
}
|
|
16911
|
+
|
|
16912
|
+
if (Date.now() - liveInteractAt > LIVE_IDLE_STOP) {
|
|
16913
|
+
return;
|
|
16914
|
+
}
|
|
16915
|
+
|
|
16916
|
+
const listEl = pickListElement(document);
|
|
16917
|
+
|
|
16918
|
+
if (!listEl) {
|
|
16919
|
+
return;
|
|
16920
|
+
}
|
|
16921
|
+
|
|
16922
|
+
liveBusy = true;
|
|
16923
|
+
liveLastCheck = Date.now();
|
|
16924
|
+
|
|
16925
|
+
try {
|
|
16926
|
+
const html = await siteFetchText(buildPageUrl(1), { credentials: 'same-origin', cache: 'no-store' }, { auto: true });
|
|
16927
|
+
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
16928
|
+
const fetched = pickListElement(doc);
|
|
16929
|
+
|
|
16930
|
+
if (!fetched || !listEl.isConnected) {
|
|
16931
|
+
return;
|
|
16932
|
+
}
|
|
16933
|
+
|
|
16934
|
+
const existing = new Set(realListItems(listEl).map(li => li.dataset.dcPostKey || listItemKey(li)));
|
|
16935
|
+
const topNo = topPostNumber(listEl);
|
|
16936
|
+
const fresh = [...fetched.children]
|
|
16937
|
+
.filter(node => isListItemNode(node) && !node.classList.contains('adv-inner'))
|
|
16938
|
+
.filter(node => {
|
|
16939
|
+
const key = listItemKey(node);
|
|
16940
|
+
|
|
16941
|
+
return key && !existing.has(key) && postNumber(key) > topNo;
|
|
16942
|
+
})
|
|
16943
|
+
.slice(0, LIVE_INSERT_MAX);
|
|
16944
|
+
|
|
16945
|
+
if (!fresh.length || !topNo) {
|
|
16946
|
+
return;
|
|
16947
|
+
}
|
|
16948
|
+
|
|
16949
|
+
const anchor = realListItems(listEl).find(li => postNumber(li.dataset.dcPostKey || listItemKey(li)) === topNo);
|
|
16950
|
+
|
|
16951
|
+
if (!anchor) {
|
|
16952
|
+
return;
|
|
16953
|
+
}
|
|
16954
|
+
|
|
16955
|
+
const scrolled = window.scrollY > 120;
|
|
16956
|
+
const before = anchor.getBoundingClientRect().top;
|
|
16957
|
+
|
|
16958
|
+
fresh.forEach(li => {
|
|
16959
|
+
const node = document.adoptNode(li);
|
|
16960
|
+
|
|
16961
|
+
prepareAppendedItem(node);
|
|
16962
|
+
node.classList.add('dc-live-new');
|
|
16963
|
+
listEl.insertBefore(node, anchor);
|
|
16964
|
+
});
|
|
16965
|
+
|
|
16966
|
+
processElements();
|
|
16967
|
+
|
|
16968
|
+
if (scrolled) {
|
|
16969
|
+
const shift = anchor.getBoundingClientRect().top - before;
|
|
16970
|
+
|
|
16971
|
+
if (shift) {
|
|
16972
|
+
window.scrollBy(0, shift);
|
|
16973
|
+
}
|
|
16974
|
+
}
|
|
16975
|
+
|
|
16976
|
+
const shown = fresh.filter(rowIsShown);
|
|
16977
|
+
|
|
16978
|
+
liveLastFound = Date.now();
|
|
16979
|
+
|
|
16980
|
+
if (shown.length) {
|
|
16981
|
+
showLivePill(shown.length, scrolled);
|
|
16982
|
+
}
|
|
16983
|
+
|
|
16984
|
+
notifyWatchHits(shown);
|
|
16985
|
+
setTimeout(() => fresh.forEach(li => li.classList.remove('dc-live-new')), 4000);
|
|
16986
|
+
} catch {
|
|
16987
|
+
} finally {
|
|
16988
|
+
liveBusy = false;
|
|
16989
|
+
}
|
|
16990
|
+
};
|
|
16991
|
+
|
|
16992
|
+
const scheduleLive = () => {
|
|
16993
|
+
clearTimeout(liveTimer);
|
|
16994
|
+
|
|
16995
|
+
if (!isLiveListPage() || document.visibilityState === 'hidden' || Date.now() - liveInteractAt > LIVE_IDLE_STOP) {
|
|
16996
|
+
return;
|
|
16997
|
+
}
|
|
16998
|
+
|
|
16999
|
+
liveTimer = setTimeout(async () => {
|
|
17000
|
+
await checkLiveList();
|
|
17001
|
+
scheduleLive();
|
|
17002
|
+
}, liveIntervalMs());
|
|
17003
|
+
};
|
|
17004
|
+
|
|
17005
|
+
const liveStatusText = () => {
|
|
17006
|
+
if (settings.liveRefresh === false) {
|
|
17007
|
+
return null;
|
|
17008
|
+
}
|
|
17009
|
+
|
|
17010
|
+
if (!isLiveListPage()) {
|
|
17011
|
+
return '갤러리 목록 1페이지에서 동작합니다.';
|
|
17012
|
+
}
|
|
17013
|
+
|
|
17014
|
+
return liveLastCheck
|
|
17015
|
+
? `마지막 확인 ${timeAgo(liveLastCheck)}${liveLastFound ? ` · 마지막 새 글 ${timeAgo(liveLastFound)}` : ''}`
|
|
17016
|
+
: '곧 첫 확인을 합니다.';
|
|
17017
|
+
};
|
|
17018
|
+
|
|
17019
|
+
const initLive = () => {
|
|
17020
|
+
const touched = () => {
|
|
17021
|
+
const wasIdle = Date.now() - liveInteractAt > LIVE_IDLE_STOP;
|
|
17022
|
+
|
|
17023
|
+
liveInteractAt = Date.now();
|
|
17024
|
+
|
|
17025
|
+
if (wasIdle) {
|
|
17026
|
+
if (Date.now() - liveLastCheck > liveIntervalMs()) {
|
|
17027
|
+
clearTimeout(liveTimer);
|
|
17028
|
+
liveTimer = setTimeout(() => checkLiveList().then(scheduleLive), 1500);
|
|
17029
|
+
} else {
|
|
17030
|
+
scheduleLive();
|
|
17031
|
+
}
|
|
17032
|
+
}
|
|
17033
|
+
};
|
|
17034
|
+
|
|
17035
|
+
['pointerdown', 'keydown', 'wheel', 'touchstart'].forEach(type => window.addEventListener(type, touched, { passive: true, capture: true }));
|
|
17036
|
+
window.addEventListener('scroll', () => {
|
|
17037
|
+
touched();
|
|
17038
|
+
|
|
17039
|
+
if (livePill && window.scrollY < 60 && livePending) {
|
|
17040
|
+
hideLivePill();
|
|
17041
|
+
}
|
|
17042
|
+
}, { passive: true });
|
|
17043
|
+
|
|
17044
|
+
document.addEventListener('visibilitychange', () => {
|
|
17045
|
+
if (document.visibilityState === 'visible') {
|
|
17046
|
+
if (!liveInteractAt) {
|
|
17047
|
+
return;
|
|
17048
|
+
}
|
|
17049
|
+
|
|
17050
|
+
liveInteractAt = Date.now();
|
|
17051
|
+
|
|
17052
|
+
if (Date.now() - liveLastCheck > liveIntervalMs()) {
|
|
17053
|
+
clearTimeout(liveTimer);
|
|
17054
|
+
liveTimer = setTimeout(() => checkLiveList().then(scheduleLive), 2500);
|
|
17055
|
+
} else {
|
|
17056
|
+
scheduleLive();
|
|
17057
|
+
}
|
|
17058
|
+
} else {
|
|
17059
|
+
clearTimeout(liveTimer);
|
|
17060
|
+
}
|
|
17061
|
+
});
|
|
17062
|
+
|
|
17063
|
+
window.addEventListener('hashchange', () => {
|
|
17064
|
+
authorFilterOff = false;
|
|
17065
|
+
applyAuthorFilter();
|
|
17066
|
+
});
|
|
17067
|
+
|
|
17068
|
+
if (Number.isFinite(window.__dcEnhancerNetScale)) {
|
|
17069
|
+
window.__dcEnhancerLiveCheck = () => {
|
|
17070
|
+
liveInteractAt = Date.now();
|
|
17071
|
+
|
|
17072
|
+
return checkLiveList();
|
|
17073
|
+
};
|
|
17074
|
+
}
|
|
17075
|
+
};
|
|
17076
|
+
|
|
17077
|
+
const collectMemosForSync = () => readMemos();
|
|
17078
|
+
|
|
17079
|
+
const mergeMemos = (local = {}, remote) => {
|
|
17080
|
+
const out = { ...(remote && typeof remote === 'object' ? remote : {}) };
|
|
17081
|
+
|
|
17082
|
+
Object.entries(local || {}).forEach(([key, item]) => {
|
|
17083
|
+
if (!out[key] || (Number(item.at) || 0) >= (Number(out[key].at) || 0)) {
|
|
17084
|
+
out[key] = item;
|
|
17085
|
+
}
|
|
17086
|
+
});
|
|
17087
|
+
|
|
17088
|
+
return out;
|
|
17089
|
+
};
|
|
17090
|
+
|
|
17091
|
+
const applyMemosFromSync = memos => {
|
|
17092
|
+
if (!memos || typeof memos !== 'object' || Array.isArray(memos)) {
|
|
17093
|
+
return false;
|
|
17094
|
+
}
|
|
17095
|
+
|
|
17096
|
+
const next = JSON.stringify(mergeMemos(readMemos(), memos));
|
|
17097
|
+
|
|
17098
|
+
if (next === JSON.stringify(readMemos())) {
|
|
17099
|
+
return false;
|
|
17100
|
+
}
|
|
17101
|
+
|
|
17102
|
+
memoCache = null;
|
|
17103
|
+
writeJson(STORAGE.memos, JSON.parse(next));
|
|
17104
|
+
refreshUserDecorations();
|
|
17105
|
+
|
|
17106
|
+
return true;
|
|
17107
|
+
};
|
|
17108
|
+
|
|
17109
|
+
const mergeWatch = (local = {}, base, remote) => ({
|
|
17110
|
+
users: mergeList(local.users || [], base ? base.users || [] : null, remote ? remote.users || [] : undefined),
|
|
17111
|
+
words: mergeList(local.words || [], base ? base.words || [] : null, remote ? remote.words || [] : undefined)
|
|
17112
|
+
});
|
|
17113
|
+
|
|
17114
|
+
const applyWatchFromSync = watch => {
|
|
17115
|
+
if (!watch || typeof watch !== 'object') {
|
|
17116
|
+
return false;
|
|
17117
|
+
}
|
|
17118
|
+
|
|
17119
|
+
const next = { users: Array.isArray(watch.users) ? watch.users : [], words: Array.isArray(watch.words) ? watch.words : [] };
|
|
17120
|
+
|
|
17121
|
+
if (JSON.stringify(next) === JSON.stringify(readWatch())) {
|
|
17122
|
+
return false;
|
|
17123
|
+
}
|
|
17124
|
+
|
|
17125
|
+
watchCache = null;
|
|
17126
|
+
writeJson(STORAGE.watch, next);
|
|
17127
|
+
refreshUserDecorations();
|
|
17128
|
+
|
|
17129
|
+
return true;
|
|
17130
|
+
};
|
|
17131
|
+
|
|
17132
|
+
const renderWatchView = area => {
|
|
17133
|
+
const watch = readWatch();
|
|
17134
|
+
const memos = Object.entries(readMemos()).filter(([, item]) => item.t).sort((a, b) => b[1].at - a[1].at);
|
|
17135
|
+
|
|
17136
|
+
area.appendChild(
|
|
17137
|
+
makeNote(
|
|
17138
|
+
'관심 키워드·관심 사용자의 새 글이 목록에 올라오면 알려 드립니다 (설정 › 목록 보기 › 새 글 자동으로 이어 붙이기). 메모는 개수 제한 없이 이 기기에 저장되고 동기화됩니다.'
|
|
17139
|
+
)
|
|
17140
|
+
);
|
|
17141
|
+
|
|
17142
|
+
area.appendChild(makeSectionTitle(`🔔 관심 키워드 (${watch.words.length})`));
|
|
17143
|
+
area.appendChild(
|
|
17144
|
+
makeInputRow('제목에 들어갈 단어 (쉼표로 여러 개)', '추가', value => {
|
|
17145
|
+
addWatchWords(value);
|
|
17146
|
+
rerenderModal();
|
|
17147
|
+
}, { maxLength: 300 })
|
|
17148
|
+
);
|
|
17149
|
+
|
|
17150
|
+
if (!watch.words.length) {
|
|
17151
|
+
area.appendChild(makeEmpty('관심 키워드가 없습니다.'));
|
|
17152
|
+
}
|
|
17153
|
+
|
|
17154
|
+
watch.words.forEach(word => {
|
|
17155
|
+
area.appendChild(
|
|
17156
|
+
makeRow({
|
|
17157
|
+
title: word,
|
|
17158
|
+
chip: '키워드',
|
|
17159
|
+
chipClass: 'scope',
|
|
17160
|
+
actions: [{
|
|
17161
|
+
label: '빼기',
|
|
17162
|
+
className: 'on',
|
|
17163
|
+
onClick: () => {
|
|
17164
|
+
const next = readWatch();
|
|
17165
|
+
|
|
17166
|
+
next.words = next.words.filter(item => item !== word);
|
|
17167
|
+
writeWatch(next);
|
|
17168
|
+
rerenderModal();
|
|
17169
|
+
}
|
|
17170
|
+
}]
|
|
17171
|
+
})
|
|
17172
|
+
);
|
|
17173
|
+
});
|
|
17174
|
+
|
|
17175
|
+
area.appendChild(makeSectionTitle(`⭐ 관심 사용자 (${watch.users.length})`));
|
|
17176
|
+
|
|
17177
|
+
if (!watch.users.length) {
|
|
17178
|
+
area.appendChild(makeEmpty('닉네임을 누른 메뉴에서 ⭐ 관심 사용자로 등록할 수 있습니다.'));
|
|
17179
|
+
}
|
|
17180
|
+
|
|
17181
|
+
watch.users.forEach(key => {
|
|
17182
|
+
const entry = parseBlockKey(key);
|
|
17183
|
+
const memo = (readMemos()[key] || {}).t;
|
|
17184
|
+
|
|
17185
|
+
area.appendChild(
|
|
17186
|
+
makeRow({
|
|
17187
|
+
title: entry.value,
|
|
17188
|
+
chip: BLOCK_TYPE_LABELS[entry.type],
|
|
17189
|
+
chipClass: BLOCK_TYPE_CLASS[entry.type],
|
|
17190
|
+
meta: memo ? `📝 ${memo}` : '',
|
|
17191
|
+
actions: [{
|
|
17192
|
+
label: '빼기',
|
|
17193
|
+
className: 'on',
|
|
17194
|
+
onClick: () => {
|
|
17195
|
+
const next = readWatch();
|
|
17196
|
+
|
|
17197
|
+
next.users = next.users.filter(item => item !== key);
|
|
17198
|
+
writeWatch(next);
|
|
17199
|
+
rerenderModal();
|
|
17200
|
+
}
|
|
17201
|
+
}]
|
|
17202
|
+
})
|
|
17203
|
+
);
|
|
17204
|
+
});
|
|
17205
|
+
|
|
17206
|
+
area.appendChild(makeSectionTitle(`📝 이용자 메모 (${memos.length})`));
|
|
17207
|
+
|
|
17208
|
+
const filterRow = makeFilterRow(memos.length);
|
|
17209
|
+
|
|
17210
|
+
if (filterRow) {
|
|
17211
|
+
area.appendChild(filterRow);
|
|
17212
|
+
}
|
|
17213
|
+
|
|
17214
|
+
if (!memos.length) {
|
|
17215
|
+
area.appendChild(makeEmpty('닉네임을 누른 메뉴의 📝 메모로 남길 수 있습니다.'));
|
|
17216
|
+
}
|
|
17217
|
+
|
|
17218
|
+
memos.forEach(([key, item]) => {
|
|
17219
|
+
const entry = parseBlockKey(key);
|
|
17220
|
+
|
|
17221
|
+
area.appendChild(
|
|
17222
|
+
makeRow({
|
|
17223
|
+
title: item.t,
|
|
17224
|
+
chip: `${BLOCK_TYPE_LABELS[entry.type]} ${entry.value}`,
|
|
17225
|
+
chipClass: BLOCK_TYPE_CLASS[entry.type],
|
|
17226
|
+
meta: timeAgo(item.at),
|
|
17227
|
+
actions: [
|
|
17228
|
+
{ label: '고치기', onClick: () => openMemoEditor([{ ...entry, key }]) },
|
|
17229
|
+
{
|
|
17230
|
+
label: '지우기',
|
|
17231
|
+
className: 'on',
|
|
17232
|
+
onClick: () => {
|
|
17233
|
+
setMemo(key, '');
|
|
17234
|
+
rerenderModal();
|
|
17235
|
+
}
|
|
17236
|
+
}
|
|
17237
|
+
]
|
|
17238
|
+
})
|
|
17239
|
+
);
|
|
17240
|
+
});
|
|
17241
|
+
|
|
17242
|
+
applyListFilter(area);
|
|
17243
|
+
};
|
|
17244
|
+
|
|
17245
|
+
(() => {
|
|
17246
|
+
const tail = ['recent', 'settings'].filter(key => MODAL_VIEWS[key]).map(key => [key, MODAL_VIEWS[key]]);
|
|
17247
|
+
|
|
17248
|
+
tail.forEach(([key]) => delete MODAL_VIEWS[key]);
|
|
17249
|
+
MODAL_VIEWS.watch = { title: '관심·메모', tab: '🔔 관심', render: renderWatchView };
|
|
17250
|
+
tail.forEach(([key, view]) => {
|
|
17251
|
+
MODAL_VIEWS[key] = view;
|
|
17252
|
+
});
|
|
17253
|
+
})();
|
|
17254
|
+
|
|
15835
17255
|
const FAB_DRAG_THRESHOLD = 8;
|
|
15836
17256
|
const FAB_EDGE_MARGIN = 6;
|
|
15837
17257
|
|
|
@@ -16809,6 +18229,7 @@
|
|
|
16809
18229
|
initSiteAlarms();
|
|
16810
18230
|
initKeyboardShortcuts();
|
|
16811
18231
|
initHoverPreview();
|
|
18232
|
+
initLive();
|
|
16812
18233
|
restoreListSnapshot();
|
|
16813
18234
|
processElements();
|
|
16814
18235
|
|