// ==UserScript==
// @name Reading mode for mobile
// @namespace Reading_mode_for_mobile
// @version 0.1.5Preview
// @description [Preview] try to let reading return to reading.
// @author 稻米鼠
// @icon https://i.v2ex.co/UuYzTkNus.png
// @supportURL https://meta.appinn.net/t/23292
// @contributionURL https://afdian.net/@daomishu
// @contributionAmount 8.88
// @antifeature payment 本脚本为付费脚本
// @match *://*/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
const version = 'v0.1.5Preview'
const opt = window.RMFM ? window.RMFM : window.RMFM={}
const addons = (opt.addons && opt.addons instanceof Array) ? opt.addons : opt.addons=[]
const rulers = (opt.rulers && opt.rulers instanceof Array) ? opt.rulers : opt.rulers=[]
const mainRulers = [
{
name: "小众软件-主站",
reg: /^https?:\/\/(www\.)?appinn\.com\/\w+/,
titleGetter: 'article div.single_post > header > h1',
concentGetter: 'article div.single_post > div.post-single-content',
},
{
name: 'Bilibili-专栏',
reg: /^https?:\/\/(www\.)?bilibili\.com\/read\/\w+/,
concentGetter: '#read-article-holder'
},
{
name: '百度经验',
reg: /^https?:\/\/jingyan\.baidu\.com\/article\/\w+/,
concentGetter: '#abstract-wp, .content-box',
contentFilter: cEl=>{
changeLazyImg(cEl, '_src')
}
},
{
name: '人民日报',
reg: /^https?:\/\/(\w+\.)?people\.com\.cn\/\w+/,
titleGetter: 'h1:not([class])',
concentGetter: '.rm_txt_con'
},
{
name: '凤凰网',
reg: /^https?:\/\/(\w+\.)?ifeng\.com\/\w+/,
concentGetter: 'div[class|=main_content]',
contentFilter: cEl=>{
changeLazyImg(cEl, 'data-lazyload')
}
},
{
name: '今日头条',
reg: /^https?:\/\/((www|m)\.)?toutiao\.com\/\w+/,
contentFilter: cEl=>{
changeLazyImg(cEl, 'data-src')
}
},
{
name: '什么值得买',
reg: /^https?:\/\/(\w+\.)?smzdm\.com\/p\/\w+/,
concentGetter: 'article'
},
]
const changeLazyImg = (el, att)=>{
el.querySelectorAll('img').forEach(img=>{
if(img.getAttribute(att)){
img.src = img.getAttribute(att)
}
})
}
mainRulers.forEach(r=>{ rulers.push(r) })
addons.unshift({ name: "default" })
const Glo = {}
const createEl = (tagName, id)=>{
const el = document.createElement(tagName)
if(id) el.id = id
return el
}
const addStyle = (parent, styCode, id)=>{
const styEl = createEl('style', id)
styEl.innerHTML = styCode
parent.appendChild(styEl)
return styEl
}
const titleGetter = (selector)=>{
if(selector){
if(typeof(selector)==="string" && selector.length){
return document.body.querySelector(selector).innerText
}
if(typeof(selector)==="function"){
return selector()
}
}
const titleH1 = document.body.querySelector('h1')
if(titleH1){
return titleH1.innerText
}
return document.title
}
const contentGetter = (selector)=>{
const selctorArray = ['article', 'main', 'content', '#article', '.article', '#main', '.main', '#content', '.content', '#post', '.post']
if(selector){
if(typeof(selector)==="function"){
return selector()
}
if(typeof(selector)==="string" && selector.length){
selctorArray.unshift(selector)
}
}
for(const s of selctorArray){
const els = document.body.querySelectorAll(s)
if(els.length){
let contentHtmlCode = ''
els.forEach(el=>{
contentHtmlCode += `<div class="rmfm-section">`+el.innerHTML+`</div>`
})
return contentHtmlCode
}
}
return document.body.innerHTML
}
const contentCleaner = (content)=>{
content.querySelectorAll('*').forEach(el=>{
if(/^(style|script|header|footer|aside|nav)$/i.test(el.tagName)){
el.parentNode.removeChild(el)
return
}
el.removeAttribute('class')
el.removeAttribute('id')
el.removeAttribute('style')
el.removeAttribute('width')
el.removeAttribute('height')
})
}
const creatRoot = ()=>{
const root = createEl('div', 'rmfm-root')
document.querySelector('html').appendChild(root)
return root
}
const runAddons = (timeName, argsObj)=>{
for(const addon of addons){
if(
addon.name
&& addon[timeName]
&& typeof(addon[timeName])==='function'
){
try {
addon[timeName](Glo, argsObj)
} catch (error) {
console.warn('RMFM addons Error: ','Addon name: '+addon.name, 'Run time: '+timeName, error)
}
}
}
}
const getAddonsStyle = (timeName)=>{
let style = ''
for(const addon of addons){
if(
addon.name
&& addon[timeName]
&& typeof(addon[timeName])==='string'
){
style += addon[timeName]
}
}
return style
}
const defAddon = (timeName, func)=>{
const defAddonObj = addons[0]
if(defAddonObj.name !== 'default'){
console.error('RMFM ERROR: default addon is not found.')
return
}
defAddonObj[timeName] = func
}
const getRuler = ()=>{
const matchRulers = []
for(const ruler of rulers){
if(ruler.reg.test(window.location.href) && ruler.name){
matchRulers.push(ruler)
}
}
matchRulers.sort((a, b)=>{
return (a.weight ? a.weight : 10)-(b.weight ? b.weight : 10)
})
const rulerResult = {}
const rulersName = []
for(const ruler of matchRulers){
Object.assign(rulerResult, ruler)
rulersName.push(ruler.name)
}
console.log('RMFM use rulers: '+rulersName.join(', ')+'.')
return rulerResult
}
const addParentContainer = (el, tag, className)=>{
const parent = document.createElement(tag)
parent.className = className;
el.parentNode.replaceChild(parent, el)
parent.appendChild(el)
return parent
}
const parentNoIndent = (el)=>{
if(el.id === 'rmfm-page-article') return
if(el.tagName === 'P'){
el.classList.add('no-indent')
return
}
parentNoIndent(el.parentElement)
}
Glo.RMFMRoot = creatRoot()
Glo.RMFMShadow = Glo.RMFMRoot.attachShadow({mode: 'closed'})
class initMainButton {
lastScrollPos = window.scrollY
constructor(){
addStyle(
Glo.RMFMShadow,
getAddonsStyle('buttonStyle'),
'style-for-main-button'
)
this.el = createEl('div', 'rmfm-main-button')
Glo.RMFMShadow.appendChild(this.el)
document.addEventListener('scroll', ()=>{
if(this.lastScrollPos<=window.scrollY || window.scrollY<=window.innerHeight/20){
this.hide()
this.lastScrollPos = window.scrollY
return
}
this.show()
this.lastScrollPos = window.scrollY
})
this.el.addEventListener('click', ()=>{
runAddons('onButtonClick', this)
})
runAddons('onButtonInit', this)
}
show(){
this.el.classList.add('show')
}
hide(){
this.el.classList.remove('show')
}
toggle(){
this.el.classList.toggle('show')
}
}
class initContentArea {
constructor(){
this.box = createEl('div', 'rmfm-content-card')
Glo.mainArea.el.appendChild(this.box)
this.header = createEl('div', 'rmfm-content-header')
this.box.appendChild(this.header)
this.menu = createEl('span', 'rmfm-content-menu')
this.menu.innerText = 'Menu'
this.header.appendChild(this.menu)
this.close = createEl('span', 'rmfm-content-close')
this.close.innerText = 'Close'
this.header.appendChild(this.close)
this.close.addEventListener('click', ()=>{ Glo.mainArea.hide() })
this.el = createEl('div', 'rmfm-content-area')
this.box.appendChild(this.el)
this.footer = createEl('div', 'rmfm-content-footer')
this.box.appendChild(this.footer)
this.verInfo = createEl('span', 'rmfm-content-version')
this.verInfo.innerText = 'RMFM '+version
this.footer.appendChild(this.verInfo)
this.addInfo = createEl('span', 'rmfm-content-addons')
this.addInfo.innerText = addons.length+' addons, '+rulers.length+' rulers.'
this.footer.appendChild(this.addInfo)
}
getContent(){
this.addInfo.innerText = addons.length+' addons, '+rulers.length+' rulers.'
const ruler = Glo.ruler = getRuler()
addStyle(
this.el,
getAddonsStyle('contentStyle'),
'style-for-this-site-from-addons'
)
if(ruler.style){
addStyle(
this.el,
ruler.style,
'style-for-this-site-from-ruler'
)
}
const titleSource = titleGetter(ruler.titleGetter)
Glo.title = {
titleSource,
title: titleSource,
el: createEl('h1', 'rmfm-page-title')
}
runAddons('titleFilter')
Glo.title.el.innerText = Glo.title.title
this.el.appendChild(Glo.title.el)
this.contentEl = createEl('div', 'rmfm-page-article')
this.contentEl.classList.add('hide')
this.contentEl.innerHTML = contentGetter(ruler.concentGetter)
this.el.appendChild(this.contentEl)
if(ruler.sourceFilter && typeof(ruler.sourceFilter)==='function'){ ruler.sourceFilter(this.contentEl) }
runAddons('sourceFilter', this.contentEl)
contentCleaner(this.contentEl)
this.contentEl.classList.remove('hide')
if(ruler.contentFilter && typeof(ruler.contentFilter)==='function'){ ruler.contentFilter(this.contentEl) }
runAddons('contentFilter', this.contentEl)
}
clearContent(){
this.el.innerHTML = ''
}
refreshContent(){
this.clearContent()
this.getContent()
}
}
class initMainArea {
eventRemover = {}
constructor(){
addStyle(
Glo.RMFMShadow,
getAddonsStyle('mainStyle'),
'style-for-reading-mode'
)
this.el = createEl('div', 'rmfm-main-area')
Glo.RMFMShadow.appendChild(this.el)
runAddons('onMainInit', this)
}
show(){
this.el.classList.remove('hide')
runAddons('onMainShow')
}
hide(){
this.el.classList.add('hide')
runAddons('onMainHide')
}
toggle(){
this.el.classList.toggle('hide')
if(this.el.classList.contains('hide')){
runAddons('onMainHide')
return
}
runAddons('onMainShow')
}
clear(){
for(const name in this.eventRemover){
this.runRemover(name)
}
this.contentEl.innerHTML = ''
}
addRemover(name, remover){
if(typeof(remover)!=='function'){
console.warn('addRemover: '+name+' is not a function.')
return
}
this.eventRemover[name] = remover
}
removeRemover(name){
delete this.eventRemover[name]
}
runRemover(name){
this.eventRemover[name]()
this.removeRemover(name)
}
}
defAddon('onButtonClick', glo=>{
if(glo.mainArea){
glo.mainArea.toggle()
return
}
glo.mainArea = new initMainArea()
glo.mainArea.show()
})
defAddon('onMainShow', glo=>{
glo.mainButton.el.classList.add('lower-level')
if(!glo.content){
glo.content = new initContentArea()
}
glo.content.getContent()
})
defAddon('onMainHide', glo=>{
glo.mainButton.el.classList.remove('lower-level')
if(glo.content){
glo.content.clearContent()
}
})
defAddon('sourceFilter', (glo, contentEl)=>{
contentEl.querySelectorAll('iframe').forEach(el => {
if(el.width && el.height){
el.setAttribute('data-frame-ratio', el.width+'/'+el.height)
}
})
})
defAddon('contentFilter', (glo, contentEl)=>{
contentEl.querySelectorAll('img, video, picture, svg, canvas, iframe, pre').forEach(el => {
if(el.tagName==='VIDEO'){
addParentContainer(el, 'div', 'block-container')
return
}
if(el.tagName==='IFRAME'){
addParentContainer(el, 'div', 'block-container')
el.width = '100%'
el.style.aspectRatio = el.getAttribute('data-frame-ratio')
return
}
if(el.tagName!=='IMG' && el.offsetWidth >= contentEl.offsetWidth/3){
addParentContainer(el, 'div', 'block-container')
return
}
const img = new Image()
img.src = el.src
img.onload = ()=>{
if(img.width >= contentEl.offsetWidth/3){
const bc = addParentContainer(el, 'div', 'block-container')
parentNoIndent(bc)
}
}
});
contentEl.querySelectorAll('p').forEach(p=>{
if(p.querySelectorAll('.block-container').length){
p.classList.add('no-indent')
}
})
contentEl.querySelectorAll('pre>*').forEach(el=>{
if(el.tagName!=='CODE' && !el.classList.contains('code-copy-button')){
el.parentElement.removeChild(el)
}
})
})
defAddon('buttonStyle', `
#rmfm-main-button {
position: fixed;
z-index: 2147483647;
right: 24px;
bottom: 24px;
width: 24px;
height: 24px;
background-color: rgba(0, 0, 0, .3);
border: 6px solid rgba(255, 255, 255, .3);
border-radius: 5vmin;
backdrop-filter: blur(12px);
box-shadow: 0 2px 3px rgba(0, 0, 0, .1);
display: none;
}
#rmfm-main-button.show {
display: block;
cursor: pointer;
}
#rmfm-main-button.lower-level {
z-index: 1999999999;
}
`)
defAddon('mainStyle', `
::-webkit-scrollbar {
width: 6px;
height: 6px;
background-color: rgba(0,0,0,0.05);
}
::-webkit-scrollbar-button {
display: none;
}
::-webkit-scrollbar-thumb {
background-color: rgba(0,0,0,0.5);
border-radius: 3px;
}
#rmfm-main-area {
--Gray_0: #333336;
--Gray_1: #666669;
--Gray_2: #99999c;
--Gray_3: #cccccf;
--Gray_4: #efeff3;
--Gray_bg: #e3e3e3;
--Red: #e06c6c;
--Light_Red: #ffacac;
--Blue: #007bda;
--Light_Blue: #6cc6e6;
--Code_BG: #282c34;
--Font_Size: 18px;
--Line_Height: 1.8;
--Space: 6px;
font-size: var(--Font_Size);
font-family: -apple-system, BlinkMacSystemFont, "Apple Color Emoji", "PingFang SC", "Hiragino Sans GB", "WenQuanYi Micro Hei", "Lantinghei SC", "Source Han Sans", "Microsoft YaHei", "Helvetica Neue", "Noto Sans CJK", Helvetica, Arial, sans-serif;
font-weight: normal;
line-height: var(--Line_Height);
color: var(--Gray_0);
position: fixed;
z-index: 2147483647;
box-sizing: border-box;
width: 100vw;
height: 100vh;
left: 0;
top: 0;
overflow: hidden;
background-color: rgba(0,0,0,0.6);
backdrop-filter: blur(12px);
}
.hide {
display: none;
}
#rmfm-content-card {
box-sizing: border-box;
width: 98vw;
max-width: 800px;
height: 98vh;
margin: 1vh auto;
padding: 0;
overflow: hidden;
background-color: #fff;
box-shadow: 0 1vmin 6vmin rgba(0,0,0,0.4);
border-radius: 4px;
}
#rmfm-content-header,
#rmfm-content-footer {
position: relative;
font-size: 14px;
line-height: calc(var(--Font_Size) * 2);
height: calc(var(--Font_Size) * 2);
overflow: hidden;
color: var(--Gray_2);
}
#rmfm-content-menu,
#rmfm-content-close,
#rmfm-content-version,
#rmfm-content-addons {
display: inline-block;
font-size: 16px;
font-weight: 200;
padding: 0 8px;
}
#rmfm-content-menu,
#rmfm-content-close {
cursor: pointer;
}
#rmfm-content-close,
#rmfm-content-addons {
float: right;
}
#rmfm-content-area {
position: relative;
box-sizing: border-box;
width: 98vw;
max-width: 800px;
height: calc(98vh - var(--Font_Size) * 4);
padding: 0;
overflow: hidden auto;
}
#rmfm-content-area h1#rmfm-page-title {
text-align: center;
}
#rmfm-content-area #rmfm-page-article {
padding: 0 calc(var(--Font_Size) * 4);
}
#rmfm-content-area * {
max-width: 100%;
}
#rmfm-content-area a {
color: var(--Red);
text-decoration: none;
padding: 0 var(--Space);
border-radius: var(--Space);
display: inline-block;
}
#rmfm-content-area a:visited {
color: var(--Red);
}
#rmfm-content-area a:hover {
color: var(--Gray_4);
background-color: var(--Red);
}
#rmfm-content-area a:active {
color: var(--Gray_4);
background-color: var(--Light_Red);
}
#rmfm-content-area p,
#rmfm-content-area h1,
#rmfm-content-area h2,
#rmfm-content-area h3,
#rmfm-content-area h4,
#rmfm-content-area h5,
#rmfm-content-area h6,
#rmfm-content-area hr,
#rmfm-content-area ul,
#rmfm-content-area ol {
margin: var(--Space) 0;
}
#rmfm-content-area p {
text-indent: 2em;
}
#rmfm-content-area p.no-indent {
text-indent: 0;
}
#rmfm-content-area p * {
text-indent: 0;
}
#rmfm-content-area p code {
color: var(--Red);
background: var(--Gray_4);
}
#rmfm-content-area code,
#rmfm-content-area pre {
font-family: Consolas, Menlo, Monaco, "Lucida Console", "Liberation Mono", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", monospace;
}
#rmfm-content-area hr {
border: none;
background: var(--Gray_4);
height: 1px;
}
#rmfm-content-area blockquote {
background-color: var(--Gray_4);
padding: calc(var(--Font_Size) / 2);
border-left: 5px solid var(--Gray_2);
}
#rmfm-content-area pre {
margin: 0;
padding: var(--Space) 0;
background-color: var(--Code_BG);
color: var(--Gray_4);
white-space: pre-wrap;
word-break: break-all;
text-align: left;
line-height: 1.2;
}
#rmfm-content-area pre code {
padding: var(--Font_Size) calc(var(--Font_Size) * 4);
position: relative;
}
#rmfm-content-area pre code::before {
content: ' ';
position: absolute;
height: 100%;
left: calc(var(--Font_Size) * 3.5);
top: 0;
z-index: 100;
border-left: 1px solid var(--Gray_3);
}
#rmfm-content-area pre code .rmfm-code-line {
position: relative;
}
#rmfm-content-area pre code .rmfm-code-line .rmfm-code-line-num {
position: absolute;
top: 0;
left: calc(var(--Font_Size) * -5);
width: calc(var(--Font_Size) * 4);
height: 100%;
background-repeat: no-repeat;
background-position: top right;
}
#rmfm-content-area .block-container {
position: relative;
width: 98vw;
max-width: 800px;
margin: var(--Space) 0;
left: 50%;
transform: translateX(-50%);
text-align: center;
}
@media screen and (max-width: 800px) {
#rmfm-content-card {
width: 100vw;
height: 100vh;
margin: 0 auto;
background-color: #fff;
box-shadow: none;
border-radius: 0;
}
#rmfm-content-area {
width: 100%;
height: calc(100vh - var(--Font_Size) * 4);
}
#rmfm-content-area #rmfm-page-article {
padding: 0 5%;
}
#rmfm-content-area .block-container {
width: 100vw;
}
}
`)
rulers.unshift({
name: 'default',
weight: 1,
reg: /^https?:\/\/.*$/
})
runAddons('onStart')
let timer_A = 0
let timer_B = 0
const starter = ()=>{
if(!timer_A){
timer_A = window.setTimeout(()=>{
window.clearTimeout(timer_A)
window.clearTimeout(timer_B)
if(Glo.mainButton) return
Glo.mainButton = new initMainButton()
}, 1200)
}
}
window.addEventListener('load', ()=>{
starter()
})
document.addEventListener('readystatechange', ()=>{
if(document.readyState == 'complete'){
starter()
}
})
if(document.readyState == 'complete'){
starter()
}
timer_B = window.setTimeout(starter, 5e3)
})();