= {
47 | [EReplaceZhidaToSearch.知乎]: 'https://www.zhihu.com/search?type=content&q=',
48 | [EReplaceZhidaToSearch.百度]: 'https://www.baidu.com/s?wd=',
49 | [EReplaceZhidaToSearch.谷歌]: 'https://www.google.com.hk/search?q=',
50 | [EReplaceZhidaToSearch.必应]: 'https://www.bing.com/search?q=',
51 | };
52 |
--------------------------------------------------------------------------------
/src/init/init-history-view.ts:
--------------------------------------------------------------------------------
1 | import { dom, myStorage } from '../tools';
2 |
3 | const CONTENT_HREF = ['www.zhihu.com/question/', 'zhuanlan.zhihu.com/p/', 'www.zhihu.com/zvideo/'];
4 |
5 | /** 添加浏览历史 */
6 | export const initHistoryView = async () => {
7 | const { href, origin, pathname } = location;
8 | // 判断是否在内容页面中(回答、文章、视频)
9 | let isContentHref = false;
10 | CONTENT_HREF.forEach((item) => href.includes(item) && (isContentHref = true));
11 | if (!isContentHref) return;
12 |
13 | setTimeout(async () => {
14 | let name = '';
15 | const isQuestion = href.includes('www.zhihu.com/question/');
16 | isQuestion &&
17 | dom('.QuestionPage [itemprop="name"]') &&
18 | (name = `「问题」${(dom('.QuestionPage [itemprop="name"]') as HTMLMetaElement)!.content}`);
19 | href.includes('zhuanlan.zhihu.com/p/') && dom('.Post-Title') && (name = `「文章」${dom('.Post-Title')!.innerText}`);
20 | href.includes('www.zhihu.com/zvideo/') && dom('.ZVideo .ZVideo-title') && (name = `「视频」${dom('.ZVideo .ZVideo-title')!.innerText}`);
21 |
22 | if (!name) {
23 | initHistoryView();
24 | return;
25 | }
26 |
27 | let extra = '';
28 | const questionAnswerId = pathname.replace(/\/question\/\d+\/answer\//, '');
29 | if (isQuestion && questionAnswerId) {
30 | extra = ` ---- 回答: ${questionAnswerId}`;
31 | }
32 |
33 | const nA = `${name + extra}`;
34 | const { view } = await myStorage.getHistory();
35 | if (!view.includes(nA)) {
36 | view.unshift(nA);
37 | myStorage.updateHistoryItem('view', view);
38 | }
39 | }, 500);
40 | };
41 |
--------------------------------------------------------------------------------
/src/init/init-html/common-html.ts:
--------------------------------------------------------------------------------
1 | import { ICommonContent } from './types';
2 |
3 | /** 提示HTML */
4 | export const createHTMLTooltip = (value: string) => `?${value}`;
5 |
6 | /** 范围选择器HTML */
7 | export const createHTMLRange = (v: string, min: number, max: number, unit = '') =>
8 | `${
9 | `当前:0${unit}` +
10 | `${min}${unit}` +
11 | `` +
12 | `${max}${unit}`
13 | }
`;
14 |
15 | /**
16 | * form box switch 通用模块HTML
17 | * @param con ICommonContent[][]
18 | * @returns string
19 | */
20 | export const createHTMLFormBoxSwitch = (con: ICommonContent[][]) =>
21 | con
22 | .map(
23 | (item) =>
24 | `${item
25 | .map(({ label, value, needFetch, tooltip }) =>
26 | createHTMLFormItem({ label, value: ``, needFetch, tooltip })
27 | )
28 | .join('')}
`
29 | )
30 | .join('');
31 |
32 | /** 创建 formItem */
33 | export const createHTMLFormItem = ({ label, value, needFetch, tooltip, extraClass }: ICreateFormItem) =>
34 | ``;
38 |
39 | interface ICreateFormItem {
40 | /** 名称 */
41 | label: string;
42 | /** 内容 */
43 | value: string;
44 | /** 是否需要接口支持 */
45 | needFetch?: boolean;
46 | /** 提示 */
47 | tooltip?: string;
48 | /** 额外的样式 */
49 | extraClass?: string;
50 | }
51 |
--------------------------------------------------------------------------------
/src/init/init-html/configs/basic-show.ts:
--------------------------------------------------------------------------------
1 | import { ICommonContent } from '../types';
2 |
3 | /** 基础设置 - 显示设置部分 */
4 | export const BASIC_SHOW: ICommonContent[][] = [
5 | [
6 | {
7 | label:
8 | `列表 - 标题类别显示` +
9 | `「问题」` +
10 | `「文章」` +
11 | `「视频」` +
12 | `「想法」`,
13 | value: 'questionTitleTag',
14 | },
15 | { label: '列表和回答 - 点击高亮边框', value: 'highlightListItem' },
16 | { label: '列表 - 「···」按钮移动到最右侧', value: 'fixedListItemMore' },
17 | { label: '列表 - 显示「直达问题」按钮', value: 'listOutputToQuestion' },
18 | ],
19 | [
20 | { label: '操作栏仅显示数字和图标', value: 'justNumberInAction' },
21 | ],
22 | [
23 | { label: '问题详情 - 替换回答顶部赞同数显示(实时显示点赞数量)', value: 'topVote' },
24 | { label: '问题详情 - 一键获取回答链接', value: 'copyAnswerLink' },
25 | { label: '回答和文章顶部显示「导出当前内容/回答按钮」', value: 'topExportContent' },
26 | ],
27 | [
28 | { label: '用户主页 - 内容发布和修改时间', value: 'userHomeContentTimeTop' },
29 | { label: '列表 - 发布和修改时间', value: 'listItemCreatedAndModifiedTime' },
30 | { label: '问题详情 - 问题 - 发布和修改时间', value: 'questionCreatedAndModifiedTime' },
31 | { label: '问题详情 - 回答 - 发布和修改时间', value: 'answerItemCreatedAndModifiedTime' },
32 | { label: '文章 - 发布时间', value: 'articleCreateTimeToTop' },
33 | ],
34 | [
35 | { label: '取消评论输入框自动聚焦', value: 'cancelCommentAutoFocus' },
36 | { label: '键盘ESC键关闭评论弹窗', value: 'keyEscCloseCommentDialog' },
37 | { label: '点击空白处关闭评论弹窗', value: 'clickMarkCloseCommentDialog' },
38 | ],
39 | ];
40 |
--------------------------------------------------------------------------------
/src/init/init-html/configs/default-function.ts:
--------------------------------------------------------------------------------
1 | /** 默认功能 */
2 | export const DEFAULT_FUNCTION = [
3 | {
4 | title: '外部链接直接跳转',
5 | commit: '知乎里所有外部链接的重定向页面去除,点击将直接跳转到外部链接,不再打开知乎外部链接提示页面',
6 | },
7 | {
8 | title: '移除登录提示弹窗',
9 | },
10 | {
11 | title: '一键移除所有屏蔽话题,点击「话题黑名单」编辑按钮出现按钮',
12 | commit: '知乎屏蔽页面每次只显示部分内容,建议解除屏蔽后刷新页面查看是否仍然存在新的屏蔽标签',
13 | },
14 | {
15 | title: '视频下载',
16 | commit: '可下载视频内容左上角将会生成一个下载按钮,点击即可下载视频',
17 | },
18 | {
19 | title: '收藏夹内容导出为 PDF(需开启接口拦截)',
20 | commit: '点击收藏夹名称上方「导出当前页内容」按钮,可导出当前页码的收藏夹详细内容',
21 | },
22 | {
23 | title: '个人主页关注订阅快捷取消关注',
24 | commit:
25 | '由于知乎接口的限制,关注及移除只能在对应页面中进行操作,所以点击「移除关注」按钮将打开页面到对应页面,取消或关注后此页面自动关闭,如果脚本未加载请刷新页面
目前仅支持「我关注的问题」、「我关注的收藏」一键移除或添回关注',
26 | },
27 | {
28 | title: '预览静态图片键盘快捷切换',
29 | commit: '静态图片点击查看大图时,如果当前回答或者文章中存在多个图片,可以使用键盘方向键左右切换图片显示',
30 | },
31 | {
32 | title: '用户主页-回答-导出当前页回答的功能(需开启接口拦截)',
33 | },
34 | {
35 | title: '用户主页-文章-导出当前页文章的功能(需开启接口拦截)',
36 | },
37 | {
38 | title: '一键邀请',
39 | commit: '问题邀请用户添加一键邀请按钮,点击可邀请所有推荐用户',
40 | },
41 | {
42 | title: '解除禁止转载的限制',
43 | commit: '无视禁止转载提示强行复制',
44 | },
45 | // {
46 | // title: '快捷键收起时修正定位',
47 | // commit: '推荐列表,快捷键收起时修正定位,解决部分情况下收起的内容在页面很上方的问题,方便阅读',
48 | // },
49 | ];
50 |
--------------------------------------------------------------------------------
/src/init/init-html/configs/filter.ts:
--------------------------------------------------------------------------------
1 | import { ICommonContent } from '../types';
2 |
3 | /** 列表内容屏蔽 */
4 | export const FILTER_LIST: ICommonContent[][] = [
5 | [{ label: '屏蔽顶部活动推广', value: 'removeTopAD' }],
6 | [{ label: '屏蔽匿名用户提出的问题', value: 'removeAnonymousQuestion', needFetch: true }],
7 | [
8 | { label: '关注列表屏蔽自己的操作', value: 'removeMyOperateAtFollow' },
9 | { label: '关注列表过滤关注人赞同回答', value: 'removeFollowVoteAnswer' },
10 | { label: '关注列表过滤关注人赞同文章', value: 'removeFollowVoteArticle' },
11 | { label: '关注列表过滤关注人关注问题', value: 'removeFollowFQuestion' },
12 | ],
13 | [
14 | { label: '列表过滤邀请回答', value: 'removeItemQuestionAsk' },
15 | { label: '列表过滤商业推广', value: 'removeItemAboutAD' },
16 | { label: '列表过滤文章', value: 'removeItemAboutArticle' },
17 | { label: '列表过滤视频', value: 'removeItemAboutVideo' },
18 | { label: '列表过滤想法', value: 'removeItemAboutPin' },
19 | ],
20 | ];
21 |
--------------------------------------------------------------------------------
/src/init/init-html/configs/high-performance.ts:
--------------------------------------------------------------------------------
1 | import { ICommonContent } from "../types";
2 |
3 |
4 | export const HIGH_PERFORMANCE: ICommonContent[][] = [
5 | [
6 | { label: '推荐列表高性能模式', value: 'highPerformanceRecommend', tooltip: '推荐列表内容最多保留50条,超出则删除之前内容' },
7 | { label: '回答页高性能模式', value: 'highPerformanceAnswer', tooltip: '回答列表最多保留30条回答,超出则删除之前回答' },
8 | ],
9 | ];
10 |
11 |
--------------------------------------------------------------------------------
/src/init/init-html/configs/index.ts:
--------------------------------------------------------------------------------
1 | export * from './basic-show';
2 | export * from './default-function';
3 | export * from './filter';
4 | export * from './high-performance';
5 |
6 |
--------------------------------------------------------------------------------
/src/init/init-html/init-html.ts:
--------------------------------------------------------------------------------
1 | import { createHTMLBackgroundSetting } from '../../components/background';
2 | import { BLOCKED_USER_COMMON } from '../../components/black-list';
3 | import { createHTMLRightTitle, initMenu } from '../../components/ctz-dialog';
4 | import { initFetchInterceptStatus } from '../../components/fetch-intercept-status-change';
5 | import { createHTMLHiddenConfig } from '../../components/hidden';
6 | import { createHTMLNotInterestedList } from '../../components/not-interested';
7 | import { createHTMLTitleICOChange } from '../../components/page-title';
8 | import { createHTMLMySelect } from '../../components/select';
9 | import { createHTMLSizeSetting } from '../../components/size';
10 | import { store } from '../../store';
11 | import { dom, domC } from '../../tools';
12 | import { INNER_HTML } from '../../web-resources';
13 | import { createHTMLFormBoxSwitch, createHTMLFormItem } from './common-html';
14 | import { BASIC_SHOW, DEFAULT_FUNCTION, FILTER_LIST, HIGH_PERFORMANCE } from './configs';
15 |
16 | /** 添加修改器内元素 */
17 | export const initHTML = () => {
18 | const nDomMain = domC('div', { id: 'CTZ_MAIN', innerHTML: INNER_HTML });
19 | // 版本号
20 | dom('.ctz-version', nDomMain)!.innerText = 'version: ' + GM_info.script.version;
21 |
22 | // 添加更多默认设置
23 | dom('#CTZ_DEFAULT_SELF', nDomMain)!.innerHTML = DEFAULT_FUNCTION.map(({ title, commit }) =>
24 | createHTMLFormItem({ label: title, value: commit || '', extraClass: 'ctz-form-box-item-vertical' })
25 | ).join('');
26 |
27 | // 添加基础设置显示修改
28 | dom('#CTZ_BASIS_SHOW_CONTENT', nDomMain)!.innerHTML = createHTMLFormBoxSwitch(BASIC_SHOW);
29 | // 高性能
30 | dom('#CTZ_HIGH_PERFORMANCE', nDomMain)!.innerHTML = createHTMLFormBoxSwitch(HIGH_PERFORMANCE);
31 | // 列表内容屏蔽
32 | dom('#CTZ_FILTER_LIST_CONTENT', nDomMain)!.innerHTML = createHTMLFormBoxSwitch(FILTER_LIST);
33 |
34 | initFetchInterceptStatus(nDomMain);
35 | initMenu(nDomMain);
36 | createHTMLTitleICOChange(nDomMain);
37 | createHTMLSizeSetting(nDomMain);
38 | createHTMLBackgroundSetting(nDomMain);
39 | createHTMLHiddenConfig(nDomMain);
40 | createHTMLMySelect(nDomMain);
41 | createHTMLRightTitle(nDomMain);
42 | createHTMLNotInterestedList()
43 |
44 | dom('#CTZ_BLACKLIST_COMMON', nDomMain)!.innerHTML += createHTMLFormBoxSwitch(BLOCKED_USER_COMMON);
45 | // echoBlockedContent(nDomMain); // 回填(渲染)黑名单内容应在 echoData 中设置,保证每次打开弹窗都是最新内容
46 | appendHomeLink(nDomMain);
47 | document.body.appendChild(nDomMain);
48 | };
49 |
50 | /** 添加个人主页跳转 */
51 | export const appendHomeLink = (domMain: HTMLElement = document.body) => {
52 | const userInfo = store.getUserInfo();
53 | const boxToZhihu = dom('.ctz-to-zhihu', domMain);
54 | if (dom('.ctz-home-link') || !userInfo || !boxToZhihu) return;
55 | const hrefUser = userInfo.url ? userInfo.url.replace('/api/v4', '') : '';
56 | if (!hrefUser) return;
57 | boxToZhihu.appendChild(
58 | domC('a', {
59 | href: hrefUser,
60 | target: '_blank',
61 | innerText: '前往个人主页',
62 | className: 'ctz-home-link ctz-button',
63 | style: 'width: 100px;',
64 | })
65 | );
66 | };
67 |
--------------------------------------------------------------------------------
/src/init/init-html/types.ts:
--------------------------------------------------------------------------------
1 | export interface ICommonContent {
2 | /** 名称 */
3 | label: string;
4 | /** config 值 */
5 | value: string;
6 | /** 是否需要开启接口拦截才能生效 */
7 | needFetch?: boolean;
8 | /** 提示 */
9 | tooltip?: string;
10 | }
11 |
--------------------------------------------------------------------------------
/src/init/init-image-preview.ts:
--------------------------------------------------------------------------------
1 | import { myPreview } from '../components/preview';
2 | import { EZoomImageType } from '../components/select';
3 | import { domA, myStorage } from '../tools';
4 |
5 | /** 加载预览图片方法,解决部分图片无法点击预览的问题 */
6 | export const initImagePreview = async () => {
7 | const { zoomImageType } = await myStorage.getConfig();
8 | const images = [domA('.TitleImage:not(.ctz-processed)'), domA('.ArticleItem-image:not(.ctz-processed)'), domA('.ztext figure .content_image:not(.ctz-processed)')];
9 | for (let i = 0, imageLen = images.length; i < imageLen; i++) {
10 | const ev = images[i];
11 | for (let index = 0, len = ev.length; index < len; index++) {
12 | const nodeItem = ev[index] as HTMLImageElement;
13 | nodeItem.classList.add('ctz-processed');
14 | const src = nodeItem.src || (nodeItem.style.backgroundImage && nodeItem.style.backgroundImage.split('("')[1].split('")')[0]);
15 | nodeItem.onclick = () => myPreview.open(src);
16 | }
17 | }
18 |
19 | if (zoomImageType === EZoomImageType.自定义尺寸) {
20 | const originImages = domA('.origin_image:not(.ctz-processed)');
21 | for (let i = 0, len = originImages.length; i < len; i++) {
22 | const nodeItem = originImages[i] as HTMLImageElement;
23 | nodeItem.src = nodeItem.getAttribute('data-original') || nodeItem.src;
24 | nodeItem.classList.add('ctz-processed');
25 | nodeItem.style.cssText = 'max-width: 100%;';
26 | }
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/src/init/init-observer-resize.ts:
--------------------------------------------------------------------------------
1 | import { canCopy } from '../components/copy';
2 | import { previewGIF } from '../components/image';
3 | import { fnJustNumberInAction } from '../components/just-number';
4 | import { initLinkChanger } from '../components/link';
5 | import { myListenAnswer } from '../components/listen-answer';
6 | import { doListenComment } from '../components/listen-comment';
7 | import { myListenList } from '../components/listen-list';
8 | import { myListenSearchListItem } from '../components/listen-search-list-item';
9 | import { changeTitle } from '../components/page-title';
10 | import { myCollectionExport } from '../components/print';
11 | import { changeSizeBeforeResize } from '../components/size';
12 | import { myListenUserHomeList } from '../components/user-home';
13 | import { HTML_HOOTS } from '../misc';
14 | import { dom, domById, myStorage, pathnameHasFn, throttle, windowResize } from '../tools';
15 | import { initImagePreview } from './init-image-preview';
16 |
17 | /** 使用 ResizeObserver 监听body高度 */
18 | export const initResizeObserver = () => {
19 | const resizeObserver = new ResizeObserver(throttle(resizeFun));
20 | resizeObserver.observe(document.body);
21 | };
22 |
23 | async function resizeFun() {
24 | if (!HTML_HOOTS.includes(location.hostname)) return;
25 | const { hiddenSearchBoxTopSearch, globalTitle } = await myStorage.getConfig();
26 | // 比较列表缓存的高度是否大于当前高度,如果大于则是从 index = 0 遍历
27 | const nodeTopStoryC = domById('TopstoryContent');
28 | if (nodeTopStoryC) {
29 | const heightTopStoryContent = nodeTopStoryC.offsetHeight;
30 | if (heightTopStoryContent < 200) {
31 | // 小于200为自动加载数据(其实初始值为141)
32 | myListenList.restart();
33 | } else {
34 | myListenList.init();
35 | }
36 | // 如果列表模块高度小于网页高度则手动触发 resize 使其加载数据
37 | heightTopStoryContent < window.innerHeight && windowResize();
38 | }
39 |
40 | initLinkChanger();
41 | previewGIF();
42 | initImagePreview();
43 | doListenComment();
44 | fnJustNumberInAction();
45 | myListenSearchListItem.init();
46 | myListenAnswer.init();
47 | myListenUserHomeList.init();
48 | canCopy();
49 | changeSizeBeforeResize();
50 | pathnameHasFn({
51 | collection: () => myCollectionExport.init(),
52 | });
53 | globalTitle !== document.title && changeTitle();
54 | const nodeSearchBarInput = dom('.SearchBar-input input') as HTMLInputElement;
55 | if (hiddenSearchBoxTopSearch && nodeSearchBarInput) {
56 | nodeSearchBarInput.placeholder = '';
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/init/init-top-event-listener.ts:
--------------------------------------------------------------------------------
1 | import { answerAddBlockButton } from '../components/black-list/add-block-button';
2 | import { addAnswerCopyLink } from '../components/link';
3 | import { addNotInterestedItem } from '../components/not-interested';
4 | import { printAnswer, printArticle } from '../components/print';
5 | import { EVideoInAnswerArticle } from '../components/select';
6 | import { updateItemTime } from '../components/time';
7 | import { CLASS_VIDEO_ONE, CLASS_VIDEO_TWO_BOX, initVideoDownload } from '../components/video';
8 | import { updateTopVote } from '../components/vote';
9 | import { fnReplaceZhidaToSearch } from '../components/zhida-to-search';
10 | import { CLASS_NOT_INTERESTED, CLASS_TO_QUESTION } from '../misc';
11 | import { doFetchNotInterested, dom, domP, myStorage } from '../tools';
12 |
13 | /** 顶部ROOT元素点击事件 */
14 | export const initRootEvent = async () => {
15 | const domRoot = dom('#root');
16 | if (!domRoot) return;
17 | domRoot.addEventListener('click', async function (event) {
18 | const config = await myStorage.getConfig();
19 | const { fetchInterceptStatus, videoInAnswerArticle } = config;
20 | const target = event.target as HTMLElement;
21 | if (videoInAnswerArticle === EVideoInAnswerArticle.修改为链接) {
22 | // 回答内容中的视频回答替换为视频链接
23 | if (target.classList.contains(CLASS_VIDEO_ONE.replace('.', '')) || target.classList.contains(CLASS_VIDEO_TWO_BOX.replace('.', ''))) {
24 | const domVideo = target.querySelector('video');
25 | const videoSrc = domVideo ? domVideo.src : '';
26 | if (!videoSrc) return;
27 | window.open(videoSrc, '_blank');
28 | }
29 | }
30 |
31 | // 点击「直达问题」按钮
32 | if (target.classList.contains(CLASS_TO_QUESTION)) {
33 | // @ts-ignore 自添加属性
34 | const { path } = target._params;
35 | path && window.open(path);
36 | }
37 |
38 | // 点击外置「不感兴趣」按钮
39 | if (target.classList.contains(CLASS_NOT_INTERESTED) && fetchInterceptStatus) {
40 | // @ts-ignore 自添加属性
41 | const { id, type, title } = target._params;
42 | doFetchNotInterested({ id, type });
43 | const nodeTopStoryItem = domP(target, 'class', 'TopstoryItem');
44 | nodeTopStoryItem && (nodeTopStoryItem.style.display = 'none');
45 | addNotInterestedItem(title)
46 | }
47 |
48 | // 点击阅读全文
49 | doReadMore(target);
50 | });
51 | };
52 |
53 | /** 点击阅读全文后的操作 */
54 | export const doReadMore = (currentDom: HTMLElement) => {
55 | const contentItem = currentDom.classList.contains('ContentItem') ? currentDom : currentDom.querySelector('.ContentItem') || domP(currentDom, 'class', 'ContentItem');
56 | if (!contentItem) return;
57 | // 展开
58 | let pageType: IPageType | undefined = undefined;
59 |
60 | const domPByClass = (name: string) => domP(currentDom, 'class', name);
61 | (domPByClass('Topstory-recommend') || domPByClass('Topstory-follow') || domPByClass('zhuanlan .css-1voxft1') || domPByClass('SearchMain')) && (pageType = 'LIST');
62 | domPByClass('Question-main') && (pageType = 'QUESTION');
63 | domPByClass('Profile-main') && (pageType = 'USER_HOME');
64 | doContentItem(pageType, contentItem as HTMLElement, true);
65 | };
66 |
67 | type IPageType = 'LIST' | 'QUESTION' | 'USER_HOME';
68 |
69 | /**
70 | * 列表、回答模块内容添加对应内容或执行了阅读全文
71 | * 例如:内容顶部显示赞同数、问题添加时间、加载视频下载方法、回答内容意见分享、替换知乎直达为搜索、添加「屏蔽用户」按钮、导出当前回答、导出当前文章等
72 | * @param config 配置
73 | * @param isRecommend 是否是列表页面
74 | * @param contentItem ContentItem
75 | * @param needTimeout 是否需要延时500ms执行
76 | */
77 | export const doContentItem = async (pageType?: IPageType, contentItem?: HTMLElement, needTimeout = false) => {
78 | if (!contentItem || !pageType) return;
79 | const { topExportContent, fetchInterceptStatus, listItemCreatedAndModifiedTime, answerItemCreatedAndModifiedTime, userHomeContentTimeTop } = await myStorage.getConfig();
80 | const doFun = () => {
81 | const doByPageType: Record = {
82 | LIST: () => {
83 | listItemCreatedAndModifiedTime && updateItemTime(contentItem);
84 | if (fetchInterceptStatus) {
85 | answerAddBlockButton(contentItem);
86 | }
87 | },
88 | QUESTION: () => {
89 | answerItemCreatedAndModifiedTime && updateItemTime(contentItem);
90 | if (fetchInterceptStatus) {
91 | answerAddBlockButton(contentItem);
92 | }
93 | },
94 | USER_HOME: () => {
95 | userHomeContentTimeTop && updateItemTime(contentItem);
96 | },
97 | };
98 |
99 | doByPageType[pageType]();
100 | updateTopVote(contentItem);
101 | initVideoDownload(contentItem);
102 | addAnswerCopyLink(contentItem);
103 | fnReplaceZhidaToSearch(contentItem);
104 | if (fetchInterceptStatus) {
105 | if (topExportContent) {
106 | printAnswer(contentItem);
107 | printArticle(contentItem);
108 | }
109 | }
110 | };
111 |
112 | // 如果是回答内容,则 parentItem 设置为 nodeItem 自身
113 | if (needTimeout) {
114 | setTimeout(doFun, 500);
115 | } else {
116 | doFun();
117 | }
118 | };
119 |
--------------------------------------------------------------------------------
/src/init/redirect.ts:
--------------------------------------------------------------------------------
1 | /** 是否需要进入重定向 */
2 | export const needRedirect = () => {
3 | const { pathname, origin } = location;
4 | const phoneQuestion = '/tardis/sogou/qus/';
5 | const phoneArt = '/tardis/zm/art/';
6 |
7 | if (pathname.includes(phoneQuestion)) {
8 | const questionId = pathname.replace(phoneQuestion, '');
9 | location.href = origin + '/question/' + questionId;
10 | return true;
11 | }
12 |
13 | if (pathname.includes(phoneArt)) {
14 | const questionId = pathname.replace(phoneArt, '');
15 | location.href = 'https://zhuanlan.zhihu.com/p/' + questionId;
16 | return true;
17 | }
18 | return false;
19 | };
20 |
--------------------------------------------------------------------------------
/src/misc/index.ts:
--------------------------------------------------------------------------------
1 | export const HTML_HOOTS = ['www.zhihu.com', 'zhuanlan.zhihu.com'];
2 | /** class: INPUT 点击元素类名 */
3 | export const CLASS_INPUT_CLICK = 'ctz-i';
4 | /** class: INPUT 修改操作元素类名 */
5 | export const CLASS_INPUT_CHANGE = 'ctz-i-change';
6 | /** class: 不感兴趣外置按钮 */
7 | export const CLASS_NOT_INTERESTED = 'ctz-not-interested';
8 | /** class: 推荐列表显示「直达问题」按钮 */
9 | export const CLASS_TO_QUESTION = 'ctz-to-question';
10 | /** class: 自定义的时间元素名称 */
11 | export const CLASS_TIME_ITEM = 'ctz-list-item-time';
12 | /** class: 列表、回答内容已经监听的类名 */
13 | export const CLASS_LISTENED = 'ctz-listened';
14 | /** ID: 额外的弹窗 */
15 | export const ID_EXTRA_DIALOG = 'CTZ_EXTRA_OUTPUT_DIALOG';
16 | /** class: 知乎评论弹窗 */
17 | export const CLASS_ZHIHU_COMMENT_DIALOG = 'css-1aq8hf9';
18 | /** html 添加额外的类名 */
19 | export const EXTRA_CLASS_HTML: Record = {
20 | 'zhuanlan.zhihu.com': 'zhuanlan',
21 | 'www.zhihu.com': 'zhihu',
22 | };
23 |
--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
1 | import { IBlockedUser } from '../components/black-list';
2 | import { EVideoInAnswerArticle } from '../components/select';
3 | import { myStorage } from '../tools';
4 | import { IJsInitialData, IZhihuAnswerTarget, IZhihuRecommendItem, IZhihuUserInfo } from '../types/zhihu';
5 |
6 | /** 回答需要移除的ID和移除信息 */
7 | interface IRecommendRemoved {
8 | id: string;
9 | message: string;
10 | }
11 |
12 | class Store {
13 | /** 用户信息 更改prev: userInfo */
14 | userInfo: IZhihuUserInfo | undefined = undefined;
15 | /** 上一个请求的 Headers */
16 | prevFetchHeaders: HeadersInit = {};
17 | /** 推荐类别过滤的内容 */
18 | removeRecommends: IRecommendRemoved[] = [];
19 | /** 评论区用户信息集合 */
20 | commendAuthors: IBlockedUser[] = [];
21 | /** 当前用户主页的回答内容 */
22 | userAnswers: any[] = [];
23 | /** 当前用户主页的文章内容 */
24 | userArticle: any[] = [];
25 | /** 回答内容过滤的项 */
26 | removeAnswers: IRecommendRemoved[] = [];
27 | /** 页面初始化的数据,取自 document.getElementById('js-initialData') */
28 | jsInitialData: IJsInitialData | undefined = undefined;
29 |
30 | constructor() {
31 | // fix this is undefined
32 | this.setUserInfo = this.setUserInfo.bind(this);
33 | this.getUserInfo = this.getUserInfo.bind(this);
34 | this.setFetchHeaders = this.setFetchHeaders.bind(this);
35 | this.getFetchHeaders = this.getFetchHeaders.bind(this);
36 | this.findRemoveRecommends = this.findRemoveRecommends.bind(this);
37 | this.getRemoveRecommends = this.getRemoveRecommends.bind(this);
38 | this.setUserAnswer = this.setUserAnswer.bind(this);
39 | this.getUserAnswer = this.getUserAnswer.bind(this);
40 | this.setUserArticle = this.setUserArticle.bind(this);
41 | this.getUserArticle = this.getUserArticle.bind(this);
42 | this.setCommentAuthors = this.setCommentAuthors.bind(this);
43 | this.getCommentAuthors = this.getCommentAuthors.bind(this);
44 | this.findRemoveAnswers = this.findRemoveAnswers.bind(this);
45 | this.getRemoveAnswers = this.getRemoveAnswers.bind(this);
46 | this.setJsInitialData = this.setJsInitialData.bind(this);
47 | this.getJsInitialData = this.getJsInitialData.bind(this);
48 | }
49 |
50 | setUserInfo(inner: IZhihuUserInfo) {
51 | this.userInfo = inner;
52 | }
53 | getUserInfo() {
54 | return this.userInfo;
55 | }
56 |
57 | setFetchHeaders(headers: HeadersInit) {
58 | this.prevFetchHeaders = headers;
59 | }
60 | getFetchHeaders() {
61 | return this.prevFetchHeaders;
62 | }
63 |
64 | async findRemoveRecommends(recommends: IZhihuRecommendItem[]) {
65 | const { removeAnonymousQuestion, removeFromYanxuan, videoInAnswerArticle } = await myStorage.getConfig();
66 | recommends.forEach((item) => {
67 | const target = item.target;
68 | if (!target) return;
69 | let message = '';
70 | // 盐选专栏回答
71 | if (removeFromYanxuan && target.paid_info) {
72 | message = '选自盐选专栏的回答';
73 | }
74 | // 匿名用于的提问
75 | if (removeAnonymousQuestion && target.question && target.question.author && !target.question.author.id) {
76 | message = '匿名用户的提问';
77 | }
78 |
79 | if (videoInAnswerArticle === EVideoInAnswerArticle.隐藏视频 && target.attachment && target.attachment.video) {
80 | message = '已删除一条视频回答';
81 | }
82 |
83 | if (message) {
84 | this.removeRecommends.push({
85 | id: String(item.target.id),
86 | message,
87 | });
88 | }
89 | });
90 | }
91 | getRemoveRecommends() {
92 | return this.removeRecommends;
93 | }
94 |
95 | setUserAnswer(data: any[]) {
96 | this.userAnswers = data;
97 | }
98 | getUserAnswer() {
99 | return this.userAnswers;
100 | }
101 | setUserArticle(data: any[]) {
102 | this.userArticle = data;
103 | }
104 | getUserArticle() {
105 | return this.userArticle;
106 | }
107 | async setCommentAuthors(authors: IBlockedUser[]) {
108 | this.commendAuthors = authors;
109 | }
110 | getCommentAuthors() {
111 | return this.commendAuthors;
112 | }
113 |
114 | async findRemoveAnswers(answers: IZhihuAnswerTarget[]) {
115 | const { removeFromYanxuan, videoInAnswerArticle } = await myStorage.getConfig();
116 | answers.forEach((item) => {
117 | let message = '';
118 | if (removeFromYanxuan && item.answerType === 'paid' && item.labelInfo) {
119 | message = '已删除一条选自盐选专栏的回答';
120 | }
121 |
122 | if (videoInAnswerArticle === EVideoInAnswerArticle.隐藏视频 && item.attachment && item.attachment.video) {
123 | message = '已删除一条视频回答';
124 | }
125 |
126 | if (message) {
127 | this.removeAnswers.push({
128 | id: item.id,
129 | message,
130 | });
131 | }
132 | });
133 | }
134 | getRemoveAnswers() {
135 | return this.removeAnswers;
136 | }
137 |
138 | setJsInitialData(data: IJsInitialData) {
139 | this.jsInitialData = data;
140 | }
141 | getJsInitialData() {
142 | return this.jsInitialData;
143 | }
144 | }
145 |
146 | export const store = new Store();
147 |
--------------------------------------------------------------------------------
/src/styles/blocked-users.less:
--------------------------------------------------------------------------------
1 | @import './common.less';
2 |
3 | #CTA_BLOCKED_USERS,
4 | #CTZ_BLOCKED_USERS_TAGS {
5 | display: flex;
6 | flex-wrap: wrap;
7 | margin: 0 -8px -8px 0;
8 | }
9 |
10 | .ctz-black-item {
11 | height: 24px;
12 | line-height: 24px;
13 | box-sizing: content-box;
14 | padding: 2px 6px;
15 | margin: 0 8px 8px 0;
16 | display: flex;
17 | align-items: center;
18 | border-radius: 4px;
19 | border: 1px solid @gray1;
20 | background: #fff;
21 | transition: all 0.2s;
22 |
23 | a:hover {
24 | color: @primary;
25 | }
26 |
27 | .ctz-remove-block {
28 | width: 24px;
29 | height: 24px;
30 | text-align: center;
31 | border-radius: 8px;
32 | cursor: pointer;
33 | font-style: normal;
34 | &:hover {
35 | background: @gray01;
36 | }
37 | }
38 | }
39 |
40 | .ctz-black-box > button,
41 | .ctz-button-black {
42 | margin-left: 8px;
43 | }
44 |
45 | .ctz-blocked-users-tag {
46 | height: 24px;
47 | line-height: 24px;
48 | box-sizing: content-box;
49 | padding: 0 6px;
50 | margin: 0 8px 8px 0;
51 | display: flex;
52 | align-items: center;
53 | border-radius: 6px;
54 | border: 1px solid @gray1;
55 | background: #fff;
56 | }
57 |
58 | .ctz-remove-blocked-tag {
59 | &:hover {
60 | color: @red1;
61 | font-weight: 600;
62 | }
63 | .Active();
64 | }
65 |
66 | .ctz-black-tag {
67 | padding: 0 6px;
68 | background: #000;
69 | color: #fff;
70 | font-size: 12px;
71 | border-radius: 4px;
72 | margin-left: 8px;
73 | display: inline-block;
74 | line-height: 22px;
75 | }
76 |
77 | .ctz-in-blocked-user-tag {
78 | margin-left: 4px;
79 | border-radius: 4px;
80 | font-size: 12px;
81 | border: 1px solid @blue1;
82 | color: @blue1;
83 | background: @blue01;
84 | height: 16px;
85 | line-height: 16px;
86 | padding: 0 4px;
87 | }
88 |
89 | .ctz-edit-user-tag,
90 | .ctz-edit-blocked-tag {
91 | display: inline-block;
92 | cursor: pointer;
93 | font-size: @FontSize;
94 | margin-left: 4px;
95 | .HoverText(@blue1);
96 | .Active();
97 | }
98 |
99 | .ctz-block-user-box {
100 | button {
101 | font-size: 12px;
102 | margin-left: 8px;
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/styles/button.less:
--------------------------------------------------------------------------------
1 | @import './common.less';
2 |
3 | .ctz-button {
4 | outline: none;
5 | position: relative;
6 | display: inline-flex;
7 | align-items: center;
8 | justify-content: center;
9 | cursor: pointer;
10 | transition: all 0.3s;
11 | user-select: none;
12 | touch-action: manipulation;
13 | font-size: @FontSize;
14 | height: 24px;
15 | padding: 0px 8px;
16 | // border-radius: 6px;
17 | border-radius: 4px;
18 | border: 1px solid transparent;
19 | background-color: #fff;
20 | border-color: @gray04;
21 | font-weight: 400;
22 | box-sizing: border-box;
23 | &:hover {
24 | font-weight: 600;
25 | background: #eeeeee;
26 | }
27 | &:active {
28 | background: #e0e0e0;
29 | font-weight: 400;
30 | }
31 | }
32 |
33 | .ctz-button.ctz-button-primary {
34 | background: @primary;
35 | color: #fff;
36 | border-color: transparent;
37 | &:hover {
38 | background: @blue3;
39 | }
40 | &:active {
41 | background: @blue1;
42 | }
43 | }
44 |
45 | .ctz-button-red {
46 | color: @red1 !important;
47 | border: 1px solid @red1 !important;
48 | &:hover {
49 | color: @red2 !important;
50 | border: 1px solid @red2 !important;
51 | }
52 | }
53 |
54 | .ctz-button:disabled {
55 | border-color: #d0d0d0;
56 | background-color: rgba(0, 0, 0, 0.08);
57 | color: #b0b0b0;
58 | cursor: not-allowed;
59 | }
60 |
--------------------------------------------------------------------------------
/src/styles/change-zhihu.less:
--------------------------------------------------------------------------------
1 | /**
2 | * 修改器修改的知乎页面
3 | */
4 | @import './common.less';
5 |
6 | // 用户修改版心宽度
7 | .Profile-mainColumn,
8 | .Collections-mainColumn,
9 | .CollectionsDetailPage-mainColumn {
10 | flex: 1;
11 | }
12 |
13 | #root {
14 | // 主页私信、同志、创作中心按钮的 margin-right 设置
15 | .css-1liaddi {
16 | margin-right: 0;
17 | }
18 | }
19 |
20 | .ContentItem-title div {
21 | display: inline;
22 | }
23 |
24 | // 原包裹 SearchBar 的 div, :empty 为内部没有元素
25 | .css-1acwmmj:empty {
26 | display: none !important;
27 | }
28 |
29 | // css-hr0k1l 为图片弹窗背景
30 | .css-hr0k1l::after {
31 | content: '点击键盘左、右按键切换图片';
32 | position: absolute;
33 | bottom: 20px;
34 | left: 50%;
35 | transform: translateX(-50%);
36 | color: #fff;
37 | }
38 |
39 | // 知乎搜索页标题下的 XXX回答·XXX浏览
40 | .HotLanding-contentItemCount.HotLanding-contentItemCountWithoutSub {
41 | margin-top: 12px;
42 | }
43 |
44 | // 悬浮收起按钮
45 | body[data-suspension-pickup='true'] {
46 | .ContentItem-actions.Sticky.is-fixed {
47 | button[data-zop-retract-question='true'] {
48 | .Hover(@hover, #fff);
49 | .Active();
50 | position: fixed;
51 | bottom: 50px;
52 | background: #fff;
53 | padding: 6px 12px;
54 | box-shadow: 0px 2px 8px #c9c9c9, 0px -2px 8px #ffffff;
55 | border-radius: 8px;
56 | }
57 | }
58 | }
59 |
60 | // 首页列表宽度优化
61 | .Topstory-container,
62 | .css-knqde,
63 | .Search-container {
64 | width: fit-content !important;
65 | }
66 |
67 | // 回答详情页面内容宽度优化
68 | .Question-main .Question-mainColumn,
69 | .QuestionHeader-main {
70 | flex: 1;
71 | }
72 |
73 | .Question-main {
74 | .List-item {
75 | border-bottom: 1px dashed #ddd;
76 | }
77 | .Question-sideColumn {
78 | margin-left: 12px;
79 | }
80 |
81 | .ListShortcut {
82 | flex: 1;
83 | .Question-mainColumn {
84 | width: initial;
85 | }
86 | }
87 | }
88 |
89 | .QuestionHeader {
90 | min-width: auto; // 用来解决小屏幕下顶部偏移的问题
91 | .QuestionHeader-content {
92 | margin: 0 auto;
93 | padding: 0;
94 | max-width: initial !important;
95 | }
96 | }
97 |
98 | // // 文章页面内容宽度优化
99 | // .zhuanlan .AuthorInfo,
100 | // .zhuanlan .css-1xy3kyp {
101 | // max-width: initial;
102 | // }
103 |
104 | // 图片
105 | .GifPlayer.isPlaying img {
106 | cursor: pointer !important;
107 | }
108 |
109 | // 解决隐藏元素顶部导航栏不居中的问题
110 | .AppHeader-inner {
111 | margin: 0 auto !important;
112 | padding: 0 !important;
113 | min-width: min-content !important;
114 | width: fit-content !important;
115 | }
116 |
117 | // 专栏部分宽度
118 | .zhuanlan {
119 | .Post-Row-Content-left {
120 | flex: 1;
121 | }
122 |
123 | .Post-Row-Content-right {
124 | margin-left: 10px;
125 | }
126 |
127 | .css-1pariuy,
128 | .css-44kk6u {
129 | max-width: none;
130 | }
131 | .css-9w3zhd {
132 | width: auto;
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/src/styles/checkbox.less:
--------------------------------------------------------------------------------
1 | @import './theme.less';
2 |
3 | @checkboxSize: 22px;
4 | @checkboxMR: 8px;
5 |
6 | .ctz-i:not(.ctz-switch)[type='checkbox'] {
7 | appearance: none;
8 | -webkit-appearance: none;
9 | -moz-appearance: none;
10 | -ms-appearance: none;
11 | -o-appearance: none;
12 | transition: all 0.2s;
13 | width: @checkboxSize;
14 | height: @checkboxSize;
15 | margin: 0;
16 | position: relative;
17 | border-radius: 4px;
18 | box-sizing: border-box;
19 | border: none;
20 | cursor: pointer;
21 |
22 | &::after {
23 | cursor: pointer;
24 | transition: all 0.2s;
25 | content: ' ';
26 | width: @checkboxSize;
27 | height: @checkboxSize;
28 | border-radius: 4px;
29 | border: 1px solid @gray04;
30 | box-sizing: border-box;
31 | left: 0px;
32 | top: 0px;
33 | z-index: 1;
34 | position: absolute;
35 | font-weight: 600;
36 | display: flex;
37 | align-items: center;
38 | justify-content: center;
39 | }
40 |
41 | &:hover::after {
42 | border-color: @hover;
43 | }
44 |
45 | &:checked::after {
46 | content: '✓';
47 | font-size: 16px;
48 | font-weight: 600;
49 | color: #fff;
50 | background: @primary;
51 | border-color: @primary;
52 | }
53 | }
54 |
55 | .ctz-checkbox-group {
56 | label {
57 | display: inline-flex !important;
58 | padding-right: 12px;
59 | div {
60 | margin-right: 12px;
61 | }
62 | &::after {
63 | content: '';
64 | height: 12px;
65 | width: 1px;
66 | background: @gray04;
67 | }
68 |
69 | &:last-of-type::after {
70 | display: none;
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/styles/fetch-intercept.less:
--------------------------------------------------------------------------------
1 | .ctz-fetch-intercept .ctz-need-fetch {
2 | display: none;
3 | }
4 |
5 | .ctz-fetch-intercept.ctz-fetch-intercept-close {
6 | color: #b0b0b0 !important;
7 | cursor: not-allowed !important;
8 | text-decoration: line-through;
9 | span.ctz-need-fetch {
10 | display: inline;
11 | }
12 | div.ctz-need-fetch {
13 | display: block;
14 | }
15 | .ctz-remove-block {
16 | cursor: not-allowed !important;
17 | }
18 | .ctz-black-item .ctz-remove-block:hover,
19 | .ctz-black-item a:hover {
20 | background: transparent !important;
21 | color: #b0b0b0 !important;
22 | }
23 | &:hover {
24 | color: #b0b0b0 !important;
25 | }
26 |
27 | .ctz-switch {
28 | background-color: rgba(0, 0, 0, 0.08);
29 | cursor: not-allowed !important;
30 | &::before {
31 | background: #ffffff !important;
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/styles/form.less:
--------------------------------------------------------------------------------
1 | .ctz-form-box {
2 | background: #e9e9e8;
3 | border: 1px solid #dfdfde;
4 | border-radius: 8px;
5 | margin-bottom: 14px;
6 | }
7 |
8 | .ctz-form-box-item {
9 | display: flex;
10 | padding: 8px 12px;
11 | min-height: 24px;
12 | position: relative;
13 | & > div:first-of-type {
14 | flex: 1;
15 | line-height: 24px;
16 | word-break: keep-all;
17 | padding-right: 12px;
18 | }
19 | & > div:nth-child(2) {
20 | display: flex;
21 | flex-wrap: wrap;
22 | align-items: center;
23 | }
24 |
25 | &::after {
26 | content: '';
27 | position: absolute;
28 | background: #e0e0df;
29 | height: 1px;
30 | width: 96%;
31 | bottom: 0;
32 | left: 50%;
33 | transform: translateX(-50%);
34 | }
35 |
36 | &:last-of-type::after {
37 | display: none;
38 | }
39 | }
40 |
41 | .ctz-form-box-item-vertical {
42 | display: block;
43 | & > div:nth-child(2) {
44 | display: block;
45 | padding-top: 4px;
46 | font-size: 12px;
47 | color: #999;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/styles/radio.less:
--------------------------------------------------------------------------------
1 | @import './theme.less';
2 |
3 | .ctz-radio-group {
4 | display: flex;
5 | label {
6 | cursor: pointer;
7 | position: relative;
8 | margin: 0 !important;
9 | div {
10 | box-sizing: border-box;
11 | padding: 0 8px;
12 | height: 24px;
13 | display: flex;
14 | align-items: center;
15 | justify-content: center;
16 | border-top: 1px solid @gray04;
17 | border-bottom: 1px solid @gray04;
18 | position: relative;
19 | &::after {
20 | content: '';
21 | position: absolute;
22 | height: 100%;
23 | width: 1px;
24 | background: @gray04;
25 | right: -0;
26 | top: 0;
27 | }
28 | }
29 | &:first-of-type {
30 | div {
31 | border-radius: 8px 0 0 8px;
32 | border-left: 1px solid @gray04;
33 | &::before {
34 | display: none;
35 | }
36 | }
37 | }
38 | &:last-of-type {
39 | div {
40 | border-radius: 0 8px 8px 0;
41 | border-right: 1px solid @gray04;
42 | &::after {
43 | display: none;
44 | }
45 | }
46 | }
47 | &:hover {
48 | div {
49 | background: @blue01;
50 | }
51 | }
52 | }
53 |
54 | input {
55 | visibility: hidden;
56 | position: absolute;
57 | }
58 |
59 | input:checked + div {
60 | background: @primary;
61 | color: #fff;
62 | border-color: @primary;
63 | z-index: 1;
64 | &::after {
65 | background: @primary;
66 | z-index: 1;
67 | }
68 | &::before {
69 | content: '';
70 | position: absolute;
71 | height: 100%;
72 | width: 1px;
73 | background: @primary;
74 | left: 0;
75 | top: 0;
76 | z-index: 1;
77 | }
78 | }
79 | }
80 |
81 | .ctz-radio {
82 | display: inline-block;
83 | padding-left: 24px;
84 | line-height: 24px;
85 | input[type='radio'] {
86 | display: none;
87 |
88 | & + div {
89 | position: relative;
90 | cursor: pointer;
91 | &::before {
92 | content: '';
93 | position: absolute;
94 | left: -20px;
95 | top: 4px;
96 | border-radius: 50%;
97 | border: 1px solid #cecece;
98 | width: 14px;
99 | height: 14px;
100 | background: #fff;
101 | box-shadow: inset 5px 5px 5px #f0f0f0, inset -5px -5px 5px #ffffff;
102 | }
103 | &::after {
104 | content: '';
105 | position: absolute;
106 | left: -16px;
107 | top: 8px;
108 | border-radius: 50%;
109 | width: 8px;
110 | height: 8px;
111 | }
112 | }
113 | &:checked {
114 | + div {
115 | &::before {
116 | background: @blue1;
117 | border-color: @blue1;
118 | box-shadow: none;
119 | }
120 | &::after {
121 | background: #fff;
122 | }
123 | }
124 | }
125 | &:focus {
126 | + div::before {
127 | box-shadow: 0 0px 8px @blue1;
128 | }
129 | }
130 | &:disabled {
131 | + div::before {
132 | border: 1px solid #cecece;
133 | box-shadow: 0 0px 4px #ddd;
134 | }
135 | }
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/src/styles/range.less:
--------------------------------------------------------------------------------
1 | @import './common.less';
2 |
3 | #CTZ_DIALOG {
4 | input[type='range'] {
5 | /* 设置滑块下面那条线的样式 */
6 | outline: none; /* 去掉点击时出现的外边框 */
7 | -webkit-appearance: none;
8 | -moz-appearance: none;
9 | appearance: none; /* 这三个是去掉那条线原有的默认样式 */
10 | height: 6px;
11 | border-radius: 8px;
12 | background: #dddddc;
13 | position: relative;
14 | box-shadow: inset 1px 1px 2px #d4d4d3, inset -1px -1px 2px #d4d4d3;
15 | &::before,
16 | &::after {
17 | content: '';
18 | background: #c6c6c5;
19 | position: absolute;
20 | height: 10px;
21 | width: 3px;
22 | border-radius: 4px;
23 | top: -2px;
24 | }
25 |
26 | &::before {
27 | left: -2px;
28 | }
29 |
30 | &::after {
31 | right: -2px;
32 | }
33 |
34 | &::-webkit-slider-thumb {
35 | /* ::-webkit-slider-thumb 是代表给滑块的样式进行变更*/
36 | -webkit-appearance: none;
37 | -moz-appearance: none;
38 | appearance: none; /* 这三个是去掉滑块原有的默认样式 */
39 | transition: all 0.2s;
40 | width: 10px;
41 | height: 25px;
42 | border-radius: 16px;
43 | background: #fff;
44 | border: 1px solid #c7c7c6;
45 | z-index: 5;
46 | // box-shadow: 3px 3px 6px #f5f5f5, -3px -3px 6px #f5f5f5;
47 | &:active {
48 | background: #f0f0f0;
49 | }
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/styles/select.less:
--------------------------------------------------------------------------------
1 | @import './common.less';
2 |
3 | .ctz-select {
4 | position: relative;
5 | width: fit-content;
6 | }
7 |
8 | .ctz-select-input {
9 | background: transparent;
10 | text-align: right;
11 | height: 22px;
12 | border-radius: 6px;
13 | border: 1px solid transparent;
14 | padding: 0 8px;
15 | line-height: 22px;
16 | cursor: pointer;
17 | &:hover {
18 | background: #ffffff;
19 | border: 1px solid #e0e0e0;
20 | }
21 | }
22 |
23 | .ctz-select-icon {
24 | margin-left: 4px;
25 | }
26 |
27 | .ctz-option-box {
28 | position: absolute;
29 | top: 24px;
30 | right: 0;
31 | background: #e9e9e8;
32 | z-index: 10;
33 | padding: 6px;
34 | border-radius: 6px;
35 | border: 1px solid #e0e0e0;
36 | box-shadow: 2px 2px 4px #dbdbdb, -2px -2px 4px #dbdbdb;
37 | }
38 |
39 | .ctz-option-item {
40 | white-space: pre;
41 | cursor: default;
42 | padding: 0 6px 0 24px;
43 | border-radius: 4px;
44 | height: 24px;
45 | line-height: 24px;
46 | position: relative;
47 | &:hover {
48 | color: #fff;
49 | background: @hover;
50 | }
51 |
52 | &[data-choose="true"] {
53 | &::before {
54 | content: '✓';
55 | position: absolute;
56 | left: 6px;
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/styles/setting-background.less:
--------------------------------------------------------------------------------
1 | @import './common.less';
2 |
3 | @BorderWidth: 4px;
4 |
5 | #CTZ_BACKGROUND {
6 | gap: 12px;
7 | }
8 |
9 | .ctz-background-item {
10 | position: relative;
11 | @width: 68px;
12 | @height: 46px;
13 |
14 | input {
15 | position: absolute;
16 | visibility: hidden;
17 | &:checked + div + div {
18 | border-color: @blue1;
19 | }
20 |
21 | &:checked + div + div + div {
22 | color: #272726;
23 | }
24 | }
25 |
26 | .ctz-background-item-div {
27 | border-radius: 8px;
28 | height: @height;
29 | width: @width;
30 | margin: @BorderWidth;
31 | }
32 |
33 | .ctz-background-item-border {
34 | height: @height;
35 | width: @width;
36 | border-radius: 12px;
37 | position: absolute;
38 | top: 0;
39 | left: 0;
40 | border: @BorderWidth solid transparent;
41 | }
42 | }
43 |
44 | .ctz-background-item-name {
45 | font-size: 12px;
46 | text-align: center;
47 | padding-top: 8px;
48 | color: #777776;
49 | }
50 |
51 | #CTZ_BACKGROUND_LIGHT,
52 | #CTZ_BACKGROUND_DARK {
53 | gap: 10px;
54 | padding: 4px 4px 24px 0;
55 |
56 | .ctz-background-item {
57 | position: relative;
58 | @r: 18px;
59 |
60 | input {
61 | position: absolute;
62 | visibility: hidden;
63 | &:checked + div + div,
64 | &:checked + div + div + div {
65 | opacity: 1;
66 | }
67 | }
68 | &-div {
69 | height: @r;
70 | width: @r;
71 | border-radius: 50%;
72 | margin: 0;
73 | }
74 |
75 | &-border {
76 | height: calc(@r - (@BorderWidth * 2));
77 | width: calc(@r - (@BorderWidth * 2));
78 | border-radius: 50%;
79 | position: absolute;
80 | top: 0;
81 | left: 0;
82 | background: #fff;
83 | opacity: 0;
84 | }
85 |
86 | &-name {
87 | font-size: 12px;
88 | text-align: center;
89 | padding-top: 8px;
90 | color: #777776;
91 | opacity: 0;
92 | position: absolute;
93 | word-break: keep-all;
94 | left: 50%;
95 | transform: translateX(-50%);
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/styles/setting-block-words.less:
--------------------------------------------------------------------------------
1 | #CTZ_BLOCK_WORDS {
2 | padding-top: 0 !important;
3 | }
4 |
5 | .ctz-block-words-content {
6 | display: flex;
7 | flex-wrap: wrap;
8 | cursor: default;
9 | margin-bottom: -4px;
10 | & > span {
11 | padding: 0px 6px;
12 | border-radius: 4px;
13 | font-size: @FontSize;
14 | margin: 0 4px 4px 0;
15 | border: 1px solid @gray04;
16 | cursor: pointer;
17 | background: #fff;
18 | &:hover {
19 | color: @waring;
20 | border-color: @waring;
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/styles/setting-default.less:
--------------------------------------------------------------------------------
1 | // 默认功能
2 | #CTZ_DEFAULT_SELF {
3 | a {
4 | color: @blue1;
5 | &:hover {
6 | color: #bbb;
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/styles/switch.less:
--------------------------------------------------------------------------------
1 | @import './common.less';
2 |
3 | .ctz-switch {
4 | width: 40px;
5 | height: 24px;
6 | position: relative;
7 | background-color: #dcdfe6;
8 | // border-radius: 20px;
9 | border-radius: 6px;
10 | background-clip: content-box;
11 | display: inline-block;
12 | appearance: none;
13 | -webkit-appearance: none;
14 | -moz-appearance: none;
15 | user-select: none;
16 | outline: none;
17 | margin: 0;
18 | cursor: pointer;
19 |
20 | &::before {
21 | content: '';
22 | position: absolute;
23 | width: 22px;
24 | height: 22px;
25 | background-color: #ffffff;
26 | // border-radius: 50%;
27 | border-radius: 5px;
28 | left: 2px;
29 | top: 0;
30 | bottom: 0;
31 | margin: auto;
32 | transition: 0.3s;
33 | }
34 |
35 | &:checked {
36 | background-color: @blue1;
37 | transition: 0.6s;
38 | }
39 |
40 | &:checked::before {
41 | left: 17px;
42 | transition: 0.3s;
43 | }
44 |
45 | &:hover::before {
46 | background: #f0f0f0;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/styles/theme.less:
--------------------------------------------------------------------------------
1 | /** 红色 **/
2 | @red1: rgb(255, 59, 48);
3 | @red2: rgb(255, 69, 58);
4 | @red3: rgb(215, 0, 21);
5 | @red4: rgb(255, 105, 97);
6 | @red01: rgba(255, 59, 48, 0.1);
7 |
8 | /** 橙色 **/
9 | @origin1: rgb(255, 149, 0);
10 | @origin2: rgb(255, 159, 10);
11 | @origin3: rgb(201, 52, 0);
12 | @origin4: rgb(255, 179, 64);
13 | @origin01: rgba(255, 179, 64, 0.1);
14 |
15 | /** 黄色 **/
16 | @yellow1: rgb(160, 90, 0);
17 | @yellow2: rgb(255, 214, 10);
18 | @yellow3: rgb(255, 204, 0);
19 | @yellow4: rgb(255, 212, 38);
20 | @yellow01: rgba(160, 90, 0, 0.1);
21 |
22 | /** 绿色 **/
23 | @green1: rgb(0, 125, 27);
24 | @green2: rgb(50, 215, 75);
25 | @green3: rgb(40, 205, 65);
26 | @green4: rgb(49, 222, 75);
27 | @green01: rgba(0, 125, 27, 0.1);
28 |
29 | /** 蓝色 **/
30 | @blue1: rgb(0, 122, 255);
31 | @blue2: rgb(10, 132, 255);
32 | @blue3: rgb(0, 64, 221);
33 | @blue4: rgb(64, 156, 255);
34 | @blue01: rgba(0, 122, 255, 0.1);
35 |
36 | /** 紫色 **/
37 | @purple1: rgb(175, 82, 222);
38 | @purple2: rgb(191, 90, 242);
39 | @purple3: rgb(173, 68, 171);
40 | @purple4: rgb(218, 143, 255);
41 | @purple01: rgba(175, 82, 222, 0.1);
42 |
43 | /** 灰色 **/
44 | @gray1: rgb(142, 142, 147);
45 | @gray2: rgb(145, 145, 157);
46 | @gray3: rgb(105, 105, 110);
47 | @gray4: rgb(152, 152, 157);
48 | @gray01: rgba(142, 142, 147, 0.1);
49 | @gray02: rgba(150, 162, 170, 0.2);
50 | @gray04: rgba(150, 162, 170, 0.4);
51 |
52 | @hover: @blue1;
53 | @primary: @blue1;
54 |
55 | @waring: @red1;
56 | @waringBg: @red01;
57 |
58 | @FontSize: 13px;
--------------------------------------------------------------------------------
/src/styles/title.less:
--------------------------------------------------------------------------------
1 | @import './common.less';
2 |
3 | .ctz-title {
4 | font-weight: bold;
5 | font-size: @FontSize;
6 | display: flex;
7 | align-items: center;
8 | height: 42px;
9 | line-height: 42px;
10 | padding-left: 10px;
11 | & > span {
12 | font-size: 12px;
13 | color: #999;
14 | padding-left: 8px;
15 | b {
16 | color: @waring;
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/styles/tooltip.less:
--------------------------------------------------------------------------------
1 | @import './common.less';
2 |
3 | .ctz-tooltip {
4 | position: relative;
5 | display: inline-block;
6 | margin-left: 4px;
7 | & > span:first-child {
8 | display: inline-block;
9 | font-size: 12px;
10 | border-radius: 50%;
11 | border: 1px solid @gray4;
12 | color: @gray4;
13 | width: 12px;
14 | height: 12px;
15 | display: inline-flex;
16 | align-items: center;
17 | justify-content: center;
18 | cursor: pointer;
19 | }
20 | & > span:last-child {
21 | display: none;
22 | position: absolute;
23 | top: 30px;
24 | left: -50px;
25 | background-color: #515151;
26 | color: #fff;
27 | padding: 8px 12px;
28 | z-index: 10;
29 | border-radius: 6px;
30 | width: max-content;
31 | line-height: 24px;
32 | &::after {
33 | content: '';
34 | width: 0;
35 | height: 0;
36 | position: absolute;
37 | border-bottom: 6px solid #515151;
38 | border-left: 8px solid transparent;
39 | border-right: 8px solid transparent;
40 | top: -6px;
41 | left: 50px;
42 | }
43 | }
44 |
45 | &:hover {
46 | & > span:first-child {
47 | border-color: @blue1;
48 | color: @blue1;
49 | }
50 |
51 | & > span:last-child {
52 | display: block;
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/tools/browser.ts:
--------------------------------------------------------------------------------
1 | /** 判断浏览器环境 */
2 | const judgeBrowserType = () => {
3 | const userAgent = navigator.userAgent;
4 | if (userAgent.includes('Firefox')) return 'Firefox';
5 | if (userAgent.includes('Edg')) return 'Edge';
6 | if (userAgent.includes('Chrome')) return 'Chrome';
7 | return 'Safari';
8 | };
9 |
10 | /** 当前浏览器是否为 Safari */
11 | export const isSafari = judgeBrowserType() === 'Safari';
12 |
--------------------------------------------------------------------------------
/src/tools/do-window-resize.ts:
--------------------------------------------------------------------------------
1 | /** 手动触发页面尺寸变更方法 */
2 | export const windowResize = () => {
3 | window.dispatchEvent(new Event('resize'));
4 | };
5 |
--------------------------------------------------------------------------------
/src/tools/dom.ts:
--------------------------------------------------------------------------------
1 | /** 获取元素 */
2 | export const dom = (n: string, find: HTMLElement | Document = document): HTMLElement | undefined => (find ? (find.querySelector(n) as HTMLElement) : undefined);
3 | /** 使用 Id 获取元素 */
4 | export const domById = (id: string): HTMLElement | undefined => document.getElementById(id) as HTMLElement;
5 | /** 获取所有元素 */
6 | export const domA = (n: string, find: HTMLElement | Document = document): NodeListOf => find.querySelectorAll(n);
7 | /**
8 | * 创建元素
9 | * attrObjs - className
10 | */
11 | export const domC = (name: string, attrObjs: Record) => {
12 | const node = document.createElement(name);
13 | for (let key in attrObjs) {
14 | // @ts-ignore
15 | node[key] = attrObjs[key];
16 | }
17 | return node;
18 | };
19 |
20 | /**
21 | * 查找父级元素
22 | * @param node 元素
23 | * @param attrName 例如 'class'
24 | * @param attrValue 例如 class 名
25 | * @returns HTMLElement | undefined
26 | */
27 | export const domP = (node: any, attrName: string, attrValue: string): HTMLElement | undefined => {
28 | const nodeP = node.parentElement as HTMLElement;
29 | if (!nodeP) return undefined;
30 | if (!attrName || !attrValue) return nodeP;
31 | if (nodeP === document.body) return undefined;
32 | const attrValueList = (nodeP.getAttribute(attrName) || '').split(' ');
33 | return attrValueList.includes(attrValue) ? nodeP : domP(nodeP, attrName, attrValue);
34 | };
35 |
36 | export const insertAfter = (newElement: any, targetElement: any) => {
37 | const parent = targetElement.parentNode;
38 | if (parent.lastChild === targetElement) {
39 | parent.appendChild(newElement);
40 | } else {
41 | parent.insertBefore(newElement, targetElement.nextSibling);
42 | }
43 | };
44 |
45 | /** 判断是否返回空字符串 */
46 | export const fnReturnStr = (str: string, isHave = false, strFalse = '') => (isHave ? str : strFalse);
47 |
48 | /** 带前缀的 log */
49 | export const fnLog = (...str: string[]) => console.log('%c「知乎修改器」', 'color: green;font-weight: bold;', ...str);
50 |
51 | /** 注入样式文件的方法 */
52 | export const fnAppendStyle = (id: string, innerHTML: string) => {
53 | const element = domById(id);
54 | element ? (element.innerHTML = innerHTML) : document.head.appendChild(domC('style', { id, type: 'text/css', innerHTML }));
55 | };
56 |
57 | /** 元素属性替换 */
58 | export const fnDomReplace = (node: any, attrObjs: Record) => {
59 | if (!node) return;
60 | for (let key in attrObjs) {
61 | node[key] = attrObjs[key];
62 | }
63 | };
64 |
65 | /**
66 | * 创建按钮,font-size: 12px
67 | * @param {string} innerHTML 按钮内容
68 | * @param {string} extraCLass 按钮额外类名
69 | * @returns {HTMLElement} 元素
70 | */
71 | export const createButtonFontSize12 = (innerHTML: string, extraCLass: string = '', extra: Record = {}): HTMLElement =>
72 | domC('button', {
73 | innerHTML,
74 | className: `ctz-button ${extraCLass}`,
75 | style: 'margin-left: 8px;font-size: 12px;',
76 | ...extra,
77 | });
78 |
--------------------------------------------------------------------------------
/src/tools/fetch.ts:
--------------------------------------------------------------------------------
1 | import { store } from '../store';
2 | import { fnLog } from './dom';
3 |
4 | /** 调用「不感兴趣」接口 */
5 | export const doFetchNotInterested = ({ id, type }: { id: string; type: string }) => {
6 | const nHeader = store.getFetchHeaders() as Record;
7 | delete nHeader['vod-authorization'];
8 | delete nHeader['content-encoding'];
9 | delete nHeader['Content-Type'];
10 | delete nHeader['content-type'];
11 | const idToNum = +id;
12 | if (String(idToNum) === 'NaN') {
13 | fnLog(`调用不感兴趣接口错误,id为NaN, 原ID:${id}`);
14 | return;
15 | }
16 | fetch('/api/v3/feed/topstory/uninterestv2', {
17 | body: `item_brief=${encodeURIComponent(JSON.stringify({ source: 'TS', type: type, id: idToNum }))}`,
18 | method: 'POST',
19 | headers: new Headers({
20 | ...nHeader,
21 | 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
22 | }),
23 | }).then((res) => res.json());
24 | };
25 |
26 | /** 拦截请求 */
27 | export const interceptionResponse = (res: Response, pathRegexp: RegExp, fn: (r: any) => void) => {
28 | if (pathRegexp.test(res.url)) {
29 | res
30 | .clone()
31 | .json()
32 | .then((r) => fn(r));
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/src/tools/format-data-to-hump.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 下划线转驼峰
3 | * if_answer_is_had ---> isAnswerIsHad
4 | */
5 | export function formatDataToHump(data: any): any {
6 | if (!data) return data;
7 | if (Array.isArray(data)) {
8 | return data.map((item) => {
9 | return typeof item === 'object' ? formatDataToHump(item) : item;
10 | });
11 | } else if (typeof data === 'object') {
12 | const nData: any = {};
13 | Object.keys(data).forEach((prevKey) => {
14 | const nKey = prevKey.replace(/\_(\w)/g, (_, $1) => $1.toUpperCase());
15 | nData[nKey] = formatDataToHump(data[prevKey]);
16 | });
17 | return nData;
18 | }
19 | return data;
20 | }
21 |
--------------------------------------------------------------------------------
/src/tools/import-file.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Input type="file" 类型的导入文件方法
3 | * @param domInput 元素
4 | * @param callBack 导入完成的回调函数
5 | */
6 | export const inputImportFile = (domInput: HTMLInputElement, callBack: (ev: ProgressEvent) => void) => {
7 | domInput.onchange = (e: Event) => {
8 | const target = e.target as HTMLInputElement;
9 | const configFile = (target.files || [])[0];
10 | if (!configFile) return;
11 | const reader = new FileReader();
12 | reader.readAsText(configFile);
13 | reader.onload = callBack;
14 | target.value = '';
15 | };
16 | };
17 |
--------------------------------------------------------------------------------
/src/tools/index.ts:
--------------------------------------------------------------------------------
1 | export * from './browser';
2 | export * from './do-window-resize';
3 | export * from './dom';
4 | export * from './fetch';
5 | export * from './format-data-to-hump';
6 | export * from './import-file';
7 | export * from './math-for-my-listens';
8 | export * from './message';
9 | export * from './mouse-event-click';
10 | export * from './pathname-fn';
11 | export * from './percent';
12 | export * from './scroll-stop-on';
13 | export * from './storage';
14 | export * from './throttle';
15 | export * from './time';
16 |
17 |
--------------------------------------------------------------------------------
/src/tools/math-for-my-listens.ts:
--------------------------------------------------------------------------------
1 | import { fnLog } from './dom';
2 |
3 | export const CTZ_HIDDEN_ITEM_CLASS = 'ctz-hidden-item';
4 |
5 | export const fnHidden = (ev: HTMLElement, msg: string) => {
6 | ev.style.display = 'none';
7 | ev.classList.add(CTZ_HIDDEN_ITEM_CLASS);
8 | fnLog(msg);
9 | };
10 |
--------------------------------------------------------------------------------
/src/tools/message.ts:
--------------------------------------------------------------------------------
1 | import { dom, domById, domC } from './dom';
2 |
3 | /** class: 消息提示弹窗 */
4 | export const CLASS_MESSAGE = 'ctz-message';
5 |
6 | const messageDoms: HTMLElement[] = [];
7 | /**
8 | * 信息提示框
9 | * @param {string} value 信息内容
10 | * @param {number} t 存在时间
11 | */
12 | export const message = (value: string, t: number = 3000) => {
13 | const time = +new Date();
14 | const classTime = `ctz-message-${time}`;
15 | const nDom = domC('div', {
16 | innerHTML: value,
17 | className: `${CLASS_MESSAGE} ${classTime}`,
18 | });
19 | const domBox = domById('CTZ_MESSAGE_BOX');
20 | if (!domBox) return;
21 | domBox.appendChild(nDom);
22 | messageDoms.push(nDom);
23 | if (messageDoms.length > 3) {
24 | const prevDom = messageDoms.shift();
25 | prevDom && domBox.removeChild(prevDom);
26 | }
27 | setTimeout(() => {
28 | const nPrevDom = dom(`.${classTime}`);
29 | if (nPrevDom) {
30 | domById('CTZ_MESSAGE_BOX')!.removeChild(nPrevDom);
31 | messageDoms.shift();
32 | }
33 | }, t);
34 | };
35 |
--------------------------------------------------------------------------------
/src/tools/mouse-event-click.ts:
--------------------------------------------------------------------------------
1 | import { isSafari } from "./browser";
2 |
3 | /**
4 | * 模拟鼠标点击
5 | * @param {HTMLElement} element 需要点击的元素
6 | */
7 | export const mouseEventClick = (element?: HTMLElement) => {
8 | if (!element) return;
9 | const myWindow = isSafari ? window : unsafeWindow;
10 | const event = new MouseEvent('click', {
11 | view: myWindow,
12 | bubbles: true,
13 | cancelable: true,
14 | });
15 | element.dispatchEvent(event);
16 | };
17 |
--------------------------------------------------------------------------------
/src/tools/pathname-fn.ts:
--------------------------------------------------------------------------------
1 | /** 判断 pathname 匹配的项并运行对应方法 */
2 | export const pathnameHasFn = (obj: Record) => {
3 | const { pathname } = location;
4 | for (let name in obj) {
5 | pathname.includes(name) && obj[name]();
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/src/tools/percent.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Promise.all 百分比进度
3 | * @param requests 异步方法数据
4 | * @param callback 回调函数
5 | */
6 | export const promisePercent = (requests: any[] = [], callback: (index: number) => void): Promise => {
7 | let index = 0;
8 | requests.forEach((item) => {
9 | item.then(() => {
10 | index++;
11 | callback(index);
12 | });
13 | });
14 | return Promise.all(requests);
15 | };
16 |
--------------------------------------------------------------------------------
/src/tools/scroll-stop-on.ts:
--------------------------------------------------------------------------------
1 | import { dom } from "./dom";
2 |
3 | /** 在打开弹窗时候停止页面滚动,只允许弹窗滚动 */
4 | export const myScroll = {
5 | stop: () => dom('body')!.classList.add('ctz-stop-scroll'),
6 | on: () => dom('body')!.classList.remove('ctz-stop-scroll'),
7 | };
--------------------------------------------------------------------------------
/src/tools/storage.ts:
--------------------------------------------------------------------------------
1 | import { SAVE_HISTORY_NUMBER } from '../config';
2 | import { IPfConfig, IPfHistory } from '../config/types';
3 |
4 | /** 使用 localStorage + GM 存储,解决跨域存储配置不同的问题 */
5 | export const myStorage = {
6 | set: async function (name: string, value: Record) {
7 | value.t = +new Date();
8 | const v = JSON.stringify(value);
9 | localStorage.setItem(name, v);
10 | await GM.setValue(name, v);
11 | },
12 | get: async function (name: string) {
13 | const config = await GM.getValue(name);
14 | const configLocal = localStorage.getItem(name);
15 | const cParse = config ? JSON.parse(config) : null;
16 | const cLParse = configLocal ? JSON.parse(configLocal) : null;
17 | if (!cParse && !cLParse) return '';
18 | if (!cParse) return configLocal;
19 | if (!cLParse) return config;
20 | if (cParse.t < cLParse.t) return configLocal;
21 | return config;
22 | },
23 | getConfig: async function (): Promise {
24 | const nConfig = await this.get('pfConfig');
25 | return Promise.resolve(nConfig ? JSON.parse(nConfig) : {});
26 | },
27 | getHistory: async function (): Promise {
28 | const nHistory = await myStorage.get('pfHistory');
29 | const h = nHistory ? JSON.parse(nHistory) : { list: [], view: [] };
30 | return Promise.resolve(h);
31 | },
32 | /** 修改配置中的值 */
33 | updateConfigItem: async function (key: string | Record, value?: any) {
34 | const config = await this.getConfig();
35 | if (typeof key === 'string') {
36 | config[key] = value;
37 | } else {
38 | for (let itemKey in key) {
39 | config[itemKey] = key[itemKey];
40 | }
41 | }
42 | await this.updateConfig(config);
43 | },
44 | /** 更新配置 */
45 | updateConfig: async function (params: IPfConfig) {
46 | await this.set('pfConfig', params);
47 | },
48 | updateHistoryItem: async function (key: 'list' | 'view', params: string[]) {
49 | const pfHistory = await this.getHistory();
50 | pfHistory[key] = params.slice(0, SAVE_HISTORY_NUMBER);
51 | await this.set('pfHistory', pfHistory);
52 | },
53 | updateHistory: async function (value: IPfHistory) {
54 | await this.set('pfHistory', value);
55 | },
56 | };
57 |
--------------------------------------------------------------------------------
/src/tools/throttle.ts:
--------------------------------------------------------------------------------
1 | /** 节流, 使用时 fn 需要为 function () {} */
2 | export function throttle(fn: Function, time = 300) {
3 | let tout: NodeJS.Timeout | undefined = undefined;
4 | return function () {
5 | clearTimeout(tout);
6 | tout = setTimeout(() => {
7 | // @ts-ignore
8 | fn.apply(this, arguments);
9 | }, time);
10 | };
11 | }
12 |
--------------------------------------------------------------------------------
/src/tools/time.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 时间格式化
3 | * @param t 传入的时间
4 | * @param f 时间格式
5 | * @param showTimeFromNow 是否显示距离当前时间
6 | */
7 | export const formatTime = (t: string | number, f = 'YYYY-MM-DD HH:mm:ss', showTimeFromNow = false): string => {
8 | if (!t) return '';
9 | const d = new Date(t);
10 | const year = d.getFullYear();
11 | const month = d.getMonth() + 1;
12 | const day = d.getDate();
13 | const hour = d.getHours();
14 | const min = d.getMinutes();
15 | const sec = d.getSeconds();
16 | const preArr = (num: number) => (String(num).length !== 2 ? '0' + String(num) : String(num));
17 |
18 | const strDate = f
19 | .replace(/YYYY/g, String(year))
20 | .replace(/MM/g, preArr(month))
21 | .replace(/DD/g, preArr(day))
22 | .replace(/HH/g, preArr(hour))
23 | .replace(/mm/g, preArr(min))
24 | .replace(/ss/g, preArr(sec));
25 |
26 | if (showTimeFromNow) {
27 | return strDate + `(${timeFromNow(t)})`;
28 | }
29 |
30 | return strDate;
31 | };
32 |
33 | export const timeFromNow = (t: string | number) => {
34 | if (!t) return '';
35 | const d = new Date(t);
36 | const year = d.getFullYear();
37 |
38 | const prevTimestamp = +new Date(t);
39 | const now = new Date();
40 | const nowTimestamp = +now;
41 | const nowYear = now.getFullYear();
42 |
43 | const fromNow = nowTimestamp - prevTimestamp;
44 |
45 | // 一分钟内
46 | if (fromNow <= 1000 * 60) {
47 | return '刚刚';
48 | }
49 |
50 | // 一小时内
51 | if (fromNow <= 1000 * 60 * 60) {
52 | return `${Math.floor(fromNow / 1000 / 60)}分钟前`;
53 | }
54 |
55 | // 一天内
56 | if (fromNow <= 1000 * 60 * 60 * 24) {
57 | return `${Math.floor(fromNow / 1000 / 60 / 60)}小时前`;
58 | }
59 |
60 | // 一个月内
61 | if (fromNow <= 1000 * 60 * 60 * 24 * 31) {
62 | return `${Math.floor(fromNow / 1000 / 60 / 60 / 24)}天前`;
63 | }
64 |
65 | // 一年内
66 | if (fromNow <= 1000 * 60 * 60 * 24 * 365) {
67 | return `${Math.floor(fromNow / 1000 / 60 / 60 / 24 / 30)}个月前`;
68 | }
69 |
70 | return `${nowYear - year}年前`;
71 | };
72 |
--------------------------------------------------------------------------------
/src/types/common.type.ts:
--------------------------------------------------------------------------------
1 | export interface IOptionItem {
2 | label: string;
3 | value: string;
4 | }
5 |
--------------------------------------------------------------------------------
/src/types/global.d.ts:
--------------------------------------------------------------------------------
1 | declare const GM: {
2 | setValue: (key: string, value: string) => Promise;
3 | getValue: (key: string) => Promise;
4 | deleteValue: (key: string) => Promise;
5 | };
6 |
7 | // declare const window: Window & {
8 | // [key: string]: any;
9 | // };
10 | declare const GM_info: ScriptGetInfo;
11 | declare const GM_registerMenuCommand: (menuName: string, callback?: () => void, options?: Record) => void;
12 |
13 | type ScriptGetInfo = {
14 | downloadMode: string;
15 | isFirstPartyIsolation?: boolean;
16 | isIncognito: boolean;
17 | sandboxMode: SandboxMode;
18 | scriptHandler: string;
19 | scriptMetaStr: string | null;
20 | scriptUpdateURL: string | null;
21 | scriptWillUpdate: boolean;
22 | version?: string;
23 | script: {
24 | antifeatures: { [antifeature: string]: { [locale: string]: string } };
25 | author: string | null;
26 | blockers: string[];
27 | connects: string[];
28 | copyright: string | null;
29 | deleted?: number | undefined;
30 | description_i18n: { [locale: string]: string } | null;
31 | description: string;
32 | downloadURL: string | null;
33 | excludes: string[];
34 | fileURL: string | null;
35 | grant: string[];
36 | header: string | null;
37 | homepage: string | null;
38 | icon: string | null;
39 | icon64: string | null;
40 | includes: string[];
41 | lastModified: number;
42 | matches: string[];
43 | name_i18n: { [locale: string]: string } | null;
44 | name: string;
45 | namespace: string | null;
46 | position: number;
47 | resources: Resource[];
48 | supportURL: string | null;
49 | system?: boolean | undefined;
50 | 'run-at': string | null;
51 | unwrap: boolean | null;
52 | updateURL: string | null;
53 | version: string;
54 | webRequest: WebRequestRule[] | null;
55 | options: {
56 | check_for_updates: boolean;
57 | comment: string | null;
58 | compatopts_for_requires: boolean;
59 | compat_wrappedjsobject: boolean;
60 | compat_metadata: boolean;
61 | compat_foreach: boolean;
62 | compat_powerful_this: boolean | null;
63 | sandbox: string | null;
64 | noframes: boolean | null;
65 | unwrap: boolean | null;
66 | run_at: string | null;
67 | tab_types: string | null;
68 | override: {
69 | use_includes: string[];
70 | orig_includes: string[];
71 | merge_includes: boolean;
72 | use_matches: string[];
73 | orig_matches: string[];
74 | merge_matches: boolean;
75 | use_excludes: string[];
76 | orig_excludes: string[];
77 | merge_excludes: boolean;
78 | use_connects: string[];
79 | orig_connects: string[];
80 | merge_connects: boolean;
81 | use_blockers: string[];
82 | orig_run_at: string | null;
83 | orig_noframes: boolean | null;
84 | };
85 | };
86 | };
87 | };
88 |
89 | type SandboxMode = 'js' | 'raw' | 'dom';
90 |
91 | type Resource = {
92 | name: string;
93 | url: string;
94 | error?: string;
95 | content?: string;
96 | meta?: string;
97 | };
98 |
99 | type WebRequestRule = {
100 | selector: { include?: string | string[]; match?: string | string[]; exclude?: string | string[] } | string;
101 | action:
102 | | string
103 | | {
104 | cancel?: boolean;
105 | redirect?:
106 | | {
107 | url: string;
108 | from?: string;
109 | to?: string;
110 | }
111 | | string;
112 | };
113 | };
114 |
115 | declare module '*.js'
116 | declare const unsafeWindow: Window;
--------------------------------------------------------------------------------
/src/types/zhihu/index.ts:
--------------------------------------------------------------------------------
1 | export * from './js-initialData.type';
2 | export * from './zhihu-answer.type';
3 | export * from './zhihu-articles.type';
4 | export * from './zhihu-recommend.type';
5 | export * from './zhihu.type';
6 |
7 |
--------------------------------------------------------------------------------
/src/types/zhihu/zhihu-answer.type.ts:
--------------------------------------------------------------------------------
1 | export interface IZhihuAnswerTarget {
2 | adminClosedComment: boolean;
3 | annotationAction: null;
4 | answerType: string;
5 | attachedInfo: string;
6 | author: Author;
7 | canComment: CanComment;
8 | collapseReason: string;
9 | collapsedBy: string;
10 | commentCount: number;
11 | commentPermission: string;
12 | content: string;
13 | contentMark: ContentMark;
14 | contentNeedTruncated: boolean;
15 | createdTime: number;
16 | decorativeLabels: any[];
17 | editableContent: string;
18 | excerpt: string;
19 | extras: string;
20 | favlistsCount: number;
21 | forceLoginWhenClickReadMore: boolean;
22 | id: string;
23 | isCollapsed: boolean;
24 | isCopyable: boolean;
25 | isJumpNative: boolean;
26 | isLabeled: boolean;
27 | isMine: boolean;
28 | isNormal: boolean;
29 | isSticky: boolean;
30 | isVisible: boolean;
31 | matrixTips: string;
32 | question: Question;
33 | reactionInstruction: ContentMark;
34 | relationship: Relationship;
35 | relevantInfo: RelevantInfo;
36 | reshipmentSettings: string;
37 | rewardInfo: RewardInfo;
38 | stickyInfo: string;
39 | suggestEdit: SuggestEdit;
40 | thanksCount: number;
41 | thumbnailInfo: ThumbnailInfo;
42 | type: string;
43 | updatedTime: number;
44 | url: string;
45 | visibleOnlyToAuthor: boolean;
46 | voteupCount: number;
47 | labelInfo?: LabelInfo;
48 | attachment?: IAttachment;
49 | }
50 |
51 | interface LabelInfo {
52 | foregroundColor: {
53 | alpha: number;
54 | group: string;
55 | };
56 | iconUrl: string;
57 | text: string;
58 | type: string;
59 | }
60 |
61 | interface Author {
62 | avatarUrl: string;
63 | avatarUrlTemplate: string;
64 | badge: any[];
65 | badgeV2: BadgeV2;
66 | exposedMedal: ExposedMedal;
67 | followerCount: number;
68 | gender: number;
69 | headline: string;
70 | id: string;
71 | isAdvertiser: boolean;
72 | isFollowed: boolean;
73 | isFollowing: boolean;
74 | isOrg: boolean;
75 | isPrivacy: boolean;
76 | name: string;
77 | type: string;
78 | url: string;
79 | urlToken: string;
80 | userType: string;
81 | }
82 |
83 | interface BadgeV2 {
84 | detailBadges: any[];
85 | icon: string;
86 | mergedBadges: any[];
87 | nightIcon: string;
88 | title: string;
89 | }
90 |
91 | interface ExposedMedal {
92 | avatarUrl: string;
93 | description: string;
94 | medalAvatarFrame: string;
95 | medalId: string;
96 | medalName: string;
97 | miniAvatarUrl: string;
98 | }
99 |
100 | interface CanComment {
101 | reason: string;
102 | status: boolean;
103 | }
104 |
105 | interface ContentMark {}
106 |
107 | interface Question {
108 | created: number;
109 | id: string;
110 | questionType: string;
111 | relationship: ContentMark;
112 | title: string;
113 | type: string;
114 | updatedTime: number;
115 | url: string;
116 | }
117 |
118 | interface Relationship {
119 | isAuthor: boolean;
120 | isAuthorized: boolean;
121 | isNothelp: boolean;
122 | isThanked: boolean;
123 | upvotedFollowees: any[];
124 | voting: number;
125 | }
126 |
127 | interface RelevantInfo {
128 | isRelevant: boolean;
129 | relevantText: string;
130 | relevantType: string;
131 | }
132 |
133 | interface RewardInfo {
134 | canOpenReward: boolean;
135 | isRewardable: boolean;
136 | rewardMemberCount: number;
137 | rewardTotalMoney: number;
138 | tagline: string;
139 | }
140 |
141 | interface SuggestEdit {
142 | reason: string;
143 | status: boolean;
144 | tip: string;
145 | title: string;
146 | unnormalDetails: UnnormalDetails;
147 | url: string;
148 | }
149 |
150 | interface UnnormalDetails {
151 | description: string;
152 | note: string;
153 | reason: string;
154 | reasonId: number;
155 | status: string;
156 | }
157 |
158 | interface ThumbnailInfo {
159 | count: number;
160 | thumbnails: Thumbnail[];
161 | type: string;
162 | }
163 |
164 | interface Thumbnail {
165 | height: number;
166 | token: string;
167 | type: string;
168 | url: string;
169 | width: number;
170 | }
171 |
172 | interface IAttachment {
173 | attachmentId: string;
174 | type: string;
175 | video: Video;
176 | }
177 |
178 | interface Video {
179 | endTime: number;
180 | parentVideoId: string;
181 | playCount: number;
182 | startTime: number;
183 | subVideoId: string;
184 | title: string;
185 | videoInfo: VideoInfo;
186 | voteupCount: number;
187 | zvideoId: string;
188 | }
189 |
190 | interface VideoInfo {
191 | duration: number;
192 | height: number;
193 | isPaid: boolean;
194 | isTrial: boolean;
195 | playCount: number;
196 | playlist: Playlist;
197 | thumbnail: string;
198 | type: string;
199 | videoId: number;
200 | width: number;
201 | }
202 |
203 | interface Playlist {
204 | fhd: Fhd;
205 | hd: Fhd;
206 | ld: Fhd;
207 | sd: Fhd;
208 | }
209 |
210 | interface Fhd {
211 | bitrate: number;
212 | height: number;
213 | url: string;
214 | width: number;
215 | }
216 |
--------------------------------------------------------------------------------
/src/types/zhihu/zhihu-articles.type.ts:
--------------------------------------------------------------------------------
1 |
2 |
3 | export interface IZhihuArticlesDataItem {
4 | updated: number;
5 | author: Author;
6 | is_labeled: boolean;
7 | vessay_info: VessayInfo;
8 | excerpt: string;
9 | admin_closed_comment: boolean;
10 | article_type: string;
11 | reaction_instruction: ReactionInstruction;
12 | id: number;
13 | voteup_count: number;
14 | upvoted_followees: any[];
15 | can_comment: CanComment;
16 | title: string;
17 | url: string;
18 | comment_permission: string;
19 | copyright_permission: string;
20 | created: number;
21 | content: string;
22 | comment_count: number;
23 | image_url: string;
24 | excerpt_title: string;
25 | voting: number;
26 | type: string;
27 | suggest_edit: SuggestEdit;
28 | is_normal: boolean;
29 | }
30 |
31 | export interface Author {
32 | avatar_url_template: string;
33 | badge: Badge[];
34 | badge_v2: BadgeV2;
35 | name: string;
36 | is_advertiser: boolean;
37 | url: string;
38 | gender: number;
39 | user_type: string;
40 | vip_info: VipInfo;
41 | headline: string;
42 | avatar_url: string;
43 | is_org: boolean;
44 | type: string;
45 | url_token: string;
46 | id: string;
47 | }
48 |
49 | export interface Badge {
50 | topics: Topic[];
51 | type: string;
52 | description: string;
53 | }
54 |
55 | export interface Topic {
56 | name: string;
57 | introduction: string;
58 | excerpt: string;
59 | url: string;
60 | followers_count: number;
61 | avatar_url: string;
62 | type: string;
63 | id: string;
64 | questions_count: number;
65 | }
66 |
67 | export interface BadgeV2 {
68 | icon: string;
69 | detail_badges: DetailBadgeElement[];
70 | night_icon: string;
71 | merged_badges: DetailBadgeElement[];
72 | title: string;
73 | }
74 |
75 | export interface DetailBadgeElement {
76 | description: string;
77 | title: string;
78 | url: string;
79 | sources: Source[];
80 | night_icon: string;
81 | detail_type: string;
82 | type: string;
83 | icon: string;
84 | }
85 |
86 | export interface Source {
87 | avatar_path: string;
88 | name: string;
89 | url: string;
90 | priority: number;
91 | token: string;
92 | avatar_url: string;
93 | type: string;
94 | id: string;
95 | description: string;
96 | }
97 |
98 | export interface VipInfo {
99 | is_vip: boolean;
100 | vip_icon: VipIcon;
101 | }
102 |
103 | export interface VipIcon {
104 | url: string;
105 | night_mode_url: string;
106 | }
107 |
108 | export interface CanComment {
109 | status: boolean;
110 | reason: string;
111 | }
112 |
113 | export interface ReactionInstruction {}
114 |
115 | export interface SuggestEdit {
116 | status: boolean;
117 | url: string;
118 | reason: string;
119 | tip: string;
120 | title: string;
121 | }
122 |
123 | export interface VessayInfo {
124 | enable_video_translate: boolean;
125 | }
126 |
--------------------------------------------------------------------------------
/src/types/zhihu/zhihu-recommend.type.ts:
--------------------------------------------------------------------------------
1 | export interface IZhihuRecommendItem {
2 | id: string;
3 | type: string;
4 | offset: number;
5 | verb: string;
6 | created_time: number;
7 | updated_time: number;
8 | target: Target;
9 | brief: string;
10 | attached_info: string;
11 | action_card: boolean;
12 | }
13 |
14 | interface Target {
15 | id: number;
16 | type: string;
17 | url: string;
18 | author: Author;
19 | created_time: number;
20 | updated_time: number;
21 | voteup_count: number;
22 | thanks_count: number;
23 | comment_count: number;
24 | is_copyable: boolean;
25 | question: Question;
26 | thumbnail: string;
27 | excerpt: string;
28 | excerpt_new: string;
29 | preview_type: string;
30 | preview_text: string;
31 | reshipment_settings: string;
32 | content: string;
33 | relationship: TargetRelationship;
34 | is_labeled: boolean;
35 | visited_count: number;
36 | thumbnails: string[];
37 | favorite_count: number;
38 | answer_type: string;
39 | paid_info?: PaidInfo;
40 | attachment?: IAttachment;
41 | }
42 |
43 | interface Author {
44 | id: string;
45 | url: string;
46 | user_type: string;
47 | url_token: string;
48 | name: string;
49 | headline: string;
50 | avatar_url: string;
51 | is_org: boolean;
52 | gender: number;
53 | followers_count: number;
54 | is_following: boolean;
55 | is_followed: boolean;
56 | }
57 |
58 | interface PaidInfo {
59 | type: string;
60 | content: string;
61 | has_purchased: boolean;
62 | }
63 |
64 | interface Question {
65 | id: number;
66 | type: string;
67 | url: string;
68 | author: Author;
69 | title: string;
70 | created: number;
71 | answer_count: number;
72 | follower_count: number;
73 | comment_count: number;
74 | bound_topic_ids: number[];
75 | is_following: boolean;
76 | excerpt: string;
77 | relationship: QuestionRelationship;
78 | detail: string;
79 | question_type: string;
80 | }
81 |
82 | interface QuestionRelationship {
83 | is_author: boolean;
84 | }
85 |
86 | interface TargetRelationship {
87 | is_thanked: boolean;
88 | is_nothelp: boolean;
89 | voting: number;
90 | }
91 |
92 | interface IAttachment {
93 | video: Video;
94 | attachment_id: string;
95 | type: string;
96 | }
97 |
98 | interface Video {
99 | zvideo_id: string;
100 | title: string;
101 | start_time: number;
102 | play_count: number;
103 | video_info: VideoInfo;
104 | parent_video_id: string;
105 | end_time: number;
106 | sub_video_id: string;
107 | voteup_count: number;
108 | }
109 |
110 | interface VideoInfo {
111 | status: string;
112 | playlist: Playlist;
113 | is_deleted: boolean;
114 | created_at: number;
115 | updated_at: number;
116 | play_count: number;
117 | width: number;
118 | id: number;
119 | duration: number;
120 | height: number;
121 | thumbnail: string;
122 | }
123 |
124 | interface Playlist {
125 | ld: HD;
126 | hd: HD;
127 | sd: HD;
128 | }
129 |
130 | interface HD {
131 | width: number;
132 | format: string;
133 | play_url: string;
134 | duration: number;
135 | height: number;
136 | size: number;
137 | }
138 |
--------------------------------------------------------------------------------
/src/types/zhihu/zhihu.type.ts:
--------------------------------------------------------------------------------
1 | export type IZhihuDataZaExtraModule = Record<'card', IZhihuDataZaExtraModuleCard>;
2 |
3 | export interface IZhihuDataZaExtraModuleCard {
4 | has_image?: boolean;
5 | has_video?: boolean;
6 | content?: IZhihuCardContent;
7 | }
8 |
9 | export interface IZhihuCardContent {
10 | type?: string;
11 | token?: string;
12 | upvote_num?: number;
13 | comment_num?: number;
14 | publish_timestamp?: any;
15 | parent_token?: string;
16 | author_member_hash_id?: string;
17 | }
18 |
19 | export interface IZhihuDataZop {
20 | authorName?: string;
21 | itemId?: number;
22 | title?: string;
23 | type?: string;
24 | }
25 |
26 | /** 用户信息 */
27 | export interface IZhihuUserInfo {
28 | id: string;
29 | url_token: string;
30 | name: string;
31 | use_default_avatar: boolean;
32 | avatar_url: string;
33 | avatar_url_template: string;
34 | is_org: boolean;
35 | type: string;
36 | url: string;
37 | user_type: string;
38 | headline: string;
39 | headline_render: string;
40 | gender: number;
41 | is_advertiser: boolean;
42 | ad_type: string;
43 | ip_info: string;
44 | vip_info: IZhihuVipInfo;
45 | account_status: any[];
46 | is_force_renamed: boolean;
47 | is_destroy_waiting: boolean;
48 | answer_count: number;
49 | question_count: number;
50 | articles_count: number;
51 | columns_count: number;
52 | zvideo_count: number;
53 | favorite_count: number;
54 | pins_count: number;
55 | voteup_count: number;
56 | thanked_count: number;
57 | following_question_count: number;
58 | available_medals_count: number;
59 | uid: string;
60 | email: string;
61 | renamed_fullname: string;
62 | default_notifications_count: number;
63 | follow_notifications_count: number;
64 | vote_thank_notifications_count: number;
65 | messages_count: number;
66 | creation_count: number;
67 | is_bind_phone: boolean;
68 | is_realname: boolean;
69 | has_applying_column: boolean;
70 | has_add_baike_summary_permission: boolean;
71 | editor_info: string[];
72 | available_message_types: string[];
73 | }
74 |
75 | export interface IZhihuVipInfo {
76 | is_vip: boolean;
77 | vip_type: number;
78 | rename_days: string[];
79 | entrance_v2: any;
80 | rename_frequency: number;
81 | rename_await_days: number;
82 | }
83 |
--------------------------------------------------------------------------------
/src/web-resources.ts:
--------------------------------------------------------------------------------
1 | export const INNER_HTML = ``;
2 | export const INNER_CSS = ``;
3 |
--------------------------------------------------------------------------------
/static/background-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/background-dark.png
--------------------------------------------------------------------------------
/static/background-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/background-light.png
--------------------------------------------------------------------------------
/static/black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/black.png
--------------------------------------------------------------------------------
/static/blocked-user-tag-edit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/blocked-user-tag-edit.png
--------------------------------------------------------------------------------
/static/blocked-user-tag-input.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/blocked-user-tag-input.png
--------------------------------------------------------------------------------
/static/cancel-comment-auto-focus-after.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/cancel-comment-auto-focus-after.png
--------------------------------------------------------------------------------
/static/cancel-comment-auto-focus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/cancel-comment-auto-focus.png
--------------------------------------------------------------------------------
/static/change-web-title.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/change-web-title.png
--------------------------------------------------------------------------------
/static/comment-image-preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/comment-image-preview.png
--------------------------------------------------------------------------------
/static/copy-link.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/copy-link.png
--------------------------------------------------------------------------------
/static/download-video.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/download-video.png
--------------------------------------------------------------------------------
/static/export-content.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/export-content.png
--------------------------------------------------------------------------------
/static/export-home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/export-home.png
--------------------------------------------------------------------------------
/static/export-to-pdf.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/export-to-pdf.png
--------------------------------------------------------------------------------
/static/filter-title-word.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/filter-title-word.png
--------------------------------------------------------------------------------
/static/font-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/font-color.png
--------------------------------------------------------------------------------
/static/font-size.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/font-size.png
--------------------------------------------------------------------------------
/static/hidden.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/hidden.png
--------------------------------------------------------------------------------
/static/history-recommend.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/history-recommend.png
--------------------------------------------------------------------------------
/static/history-view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/history-view.png
--------------------------------------------------------------------------------
/static/home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/home.png
--------------------------------------------------------------------------------
/static/image-size.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/image-size.png
--------------------------------------------------------------------------------
/static/invite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/invite.png
--------------------------------------------------------------------------------
/static/item-date.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/item-date.png
--------------------------------------------------------------------------------
/static/item-type.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/item-type.png
--------------------------------------------------------------------------------
/static/just-number.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/just-number.png
--------------------------------------------------------------------------------
/static/not-fetch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/not-fetch.png
--------------------------------------------------------------------------------
/static/remove-filter-tag.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/remove-filter-tag.png
--------------------------------------------------------------------------------
/static/remove-item.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/remove-item.png
--------------------------------------------------------------------------------
/static/remove-message.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/remove-message.png
--------------------------------------------------------------------------------
/static/replace-zhida.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/replace-zhida.png
--------------------------------------------------------------------------------
/static/safari-use.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/safari-use.png
--------------------------------------------------------------------------------
/static/setting-background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/setting-background.png
--------------------------------------------------------------------------------
/static/setting-filter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/setting-filter.png
--------------------------------------------------------------------------------
/static/setting-replace-zhida.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/setting-replace-zhida.png
--------------------------------------------------------------------------------
/static/setting-size.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/setting-size.png
--------------------------------------------------------------------------------
/static/suspension-pickup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/suspension-pickup.png
--------------------------------------------------------------------------------
/static/video-hidden.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/video-hidden.png
--------------------------------------------------------------------------------
/static/video-link.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuyubing233/zhihu-custom/44b9f24950f26318dc2e73a1c46815b845e6dd6f/static/video-link.png
--------------------------------------------------------------------------------