mirror of
https://github.com/yang991178/fluent-reader.git
synced 2025-01-30 00:55:09 +01:00
add rtl and vertical article text directions
This commit is contained in:
parent
4e842e6420
commit
cf38cc00c8
29
dist/article/article.css
vendored
29
dist/article/article.css
vendored
@ -2,14 +2,25 @@
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", "Source Han Sans SC Regular", "Microsoft YaHei",
|
||||
sans-serif;
|
||||
}
|
||||
html {
|
||||
body {
|
||||
padding: 12px 96px 32px;
|
||||
overflow: hidden scroll;
|
||||
}
|
||||
body.rtl {
|
||||
direction: rtl;
|
||||
}
|
||||
body.vertical {
|
||||
padding: 32px;
|
||||
padding-right: 96px;
|
||||
writing-mode: vertical-rl;
|
||||
overflow: scroll hidden;
|
||||
}
|
||||
|
||||
:root {
|
||||
margin: 12px 96px 32px;
|
||||
--gray: #484644;
|
||||
--primary: #0078d4;
|
||||
--primary-alt: #004578;
|
||||
@ -58,6 +69,11 @@ a:active {
|
||||
margin: 0 auto;
|
||||
display: none;
|
||||
}
|
||||
body.vertical #main {
|
||||
max-width: unset;
|
||||
max-height: 700px;
|
||||
margin: auto 0;
|
||||
}
|
||||
#main.show {
|
||||
display: block;
|
||||
animation-name: fadeIn;
|
||||
@ -80,12 +96,21 @@ a:active {
|
||||
article {
|
||||
line-height: 1.6;
|
||||
}
|
||||
body.vertical article {
|
||||
line-height: 1.5;
|
||||
}
|
||||
body.vertical article p {
|
||||
text-indent: 2rem;
|
||||
}
|
||||
article * {
|
||||
max-width: 100%;
|
||||
}
|
||||
article img {
|
||||
height: auto;
|
||||
}
|
||||
body.vertical article img {
|
||||
max-height: 75%;
|
||||
}
|
||||
article figure {
|
||||
margin: 16px 0;
|
||||
text-align: center;
|
||||
|
4
dist/article/article.html
vendored
4
dist/article/article.html
vendored
@ -3,14 +3,14 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none'; script-src-elem 'sha256-sLDWrq1tUAO8IyyqmUckFqxbXYfZ2/3TEUmtxH8Unf0=' 'sha256-9YXu4Ifpt+hDzuBhE+vFtXKt1ZRbo/CkuUY4VX4dZyE='; img-src http: https: data:; style-src 'self' 'unsafe-inline'; frame-src http: https:; media-src http: https:; connect-src https: http:">
|
||||
content="default-src 'none'; script-src-elem 'sha256-sLDWrq1tUAO8IyyqmUckFqxbXYfZ2/3TEUmtxH8Unf0=' 'sha256-OOzJH14UHiDzBx2LDF/KOAR+BVrHdNzZ2fZvPaqCMhY='; img-src http: https: data:; style-src 'self' 'unsafe-inline'; frame-src http: https:; media-src http: https:; connect-src https: http:">
|
||||
<title>Article</title>
|
||||
<link rel="stylesheet" href="article.css" />
|
||||
<script integrity="sha256-sLDWrq1tUAO8IyyqmUckFqxbXYfZ2/3TEUmtxH8Unf0=" src="mercury.web.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="main"></div>
|
||||
<script integrity="sha256-9YXu4Ifpt+hDzuBhE+vFtXKt1ZRbo/CkuUY4VX4dZyE=" src="article.js"></script>
|
||||
<script integrity="sha256-OOzJH14UHiDzBx2LDF/KOAR+BVrHdNzZ2fZvPaqCMhY=" src="article.js"></script>
|
||||
<!-- Run "cat article.js | openssl dgst -sha256 -binary | openssl enc -base64 -A" for hash -->
|
||||
</body>
|
||||
</html>
|
10
dist/article/article.js
vendored
10
dist/article/article.js
vendored
@ -2,6 +2,15 @@ function get(name) {
|
||||
if (name = (new RegExp('[?&]' + encodeURIComponent(name) + '=([^&]*)')).exec(location.search))
|
||||
return decodeURIComponent(name[1]);
|
||||
}
|
||||
let dir = get("d")
|
||||
if (dir === "1") {
|
||||
document.body.classList.add("rtl")
|
||||
} else if (dir === "2") {
|
||||
document.body.classList.add("vertical")
|
||||
document.body.addEventListener("wheel", (evt) => {
|
||||
document.scrollingElement.scrollLeft -= evt.deltaY;
|
||||
});
|
||||
}
|
||||
async function getArticle(url) {
|
||||
let article = get("a")
|
||||
if (get("m") === "1") {
|
||||
@ -32,4 +41,3 @@ getArticle(url).then(article => {
|
||||
main.innerHTML = dom.body.innerHTML
|
||||
main.classList.add("show")
|
||||
})
|
||||
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
Icon,
|
||||
Link,
|
||||
} from "@fluentui/react"
|
||||
import { RSSSource, SourceOpenTarget } from "../scripts/models/source"
|
||||
import { RSSSource, SourceOpenTarget, SourceTextDirection } from "../scripts/models/source"
|
||||
import { shareSubmenu } from "./context-menu"
|
||||
import { platformCtrl, decodeFetchResponse } from "../scripts/utils"
|
||||
|
||||
@ -31,6 +31,10 @@ type ArticleProps = {
|
||||
textMenu: (position: [number, number], text: string, url: string) => void
|
||||
imageMenu: (position: [number, number]) => void
|
||||
dismissContextMenu: () => void
|
||||
updateSourceTextDirection: (
|
||||
source: RSSSource,
|
||||
direction: SourceTextDirection
|
||||
) => void
|
||||
}
|
||||
|
||||
type ArticleState = {
|
||||
@ -82,6 +86,39 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
||||
})),
|
||||
})
|
||||
|
||||
updateTextDirection = (direction: SourceTextDirection) => {
|
||||
this.props.updateSourceTextDirection(this.props.source, direction)
|
||||
}
|
||||
|
||||
directionMenuProps = (): IContextualMenuProps => ({
|
||||
items: [
|
||||
{
|
||||
key: "LTR",
|
||||
text: intl.get("article.LTR"),
|
||||
iconProps: { iconName: "Forward" },
|
||||
canCheck: true,
|
||||
checked: this.props.source.textDir === SourceTextDirection.LTR,
|
||||
onClick: () => this.updateTextDirection(SourceTextDirection.LTR),
|
||||
},
|
||||
{
|
||||
key: "RTL",
|
||||
text: intl.get("article.RTL"),
|
||||
iconProps: { iconName: "Back" },
|
||||
canCheck: true,
|
||||
checked: this.props.source.textDir === SourceTextDirection.RTL,
|
||||
onClick: () => this.updateTextDirection(SourceTextDirection.RTL),
|
||||
},
|
||||
{
|
||||
key: "Vertical",
|
||||
text: intl.get("article.Vertical"),
|
||||
iconProps: { iconName: "Down" },
|
||||
canCheck: true,
|
||||
checked: this.props.source.textDir === SourceTextDirection.Vertical,
|
||||
onClick: () => this.updateTextDirection(SourceTextDirection.Vertical),
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
moreMenuProps = (): IContextualMenuProps => ({
|
||||
items: [
|
||||
{
|
||||
@ -122,6 +159,13 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
||||
disabled: this.state.loadWebpage,
|
||||
subMenuProps: this.fontMenuProps(),
|
||||
},
|
||||
{
|
||||
key: "directionMenu",
|
||||
text: intl.get("article.textDir"),
|
||||
iconProps: { iconName: "ChangeEntitlements" },
|
||||
disabled: this.state.loadWebpage,
|
||||
subMenuProps: this.directionMenuProps(),
|
||||
},
|
||||
{
|
||||
key: "divider_1",
|
||||
itemType: ContextualMenuItemType.Divider,
|
||||
@ -290,7 +334,11 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
||||
</>
|
||||
)
|
||||
)
|
||||
return `article/article.html?a=${a}&h=${h}&s=${this.state.fontSize}&u=${
|
||||
return `article/article.html?a=${a}&h=${h}&s=${
|
||||
this.state.fontSize
|
||||
}&d=${
|
||||
this.props.source.textDir
|
||||
}&u=${
|
||||
this.props.item.link
|
||||
}&m=${this.state.loadFull ? 1 : 0}`
|
||||
}
|
||||
@ -400,7 +448,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
||||
? this.props.item.link
|
||||
: this.articleView()
|
||||
}
|
||||
webpreferences="contextIsolation,disableDialogs,autoplayPolicy=document-user-activation-required"
|
||||
webpreferences="contextIsolation,disableDialogs,autoplayPolicy=document-user-activation-required,nativeWindowOpen=false"
|
||||
partition={this.state.loadWebpage ? "sandbox" : undefined}
|
||||
/>
|
||||
)}
|
||||
|
@ -17,6 +17,7 @@ import {
|
||||
closeContextMenu,
|
||||
openImageMenu,
|
||||
} from "../scripts/models/app"
|
||||
import { RSSSource, SourceTextDirection, updateSource } from "../scripts/models/source"
|
||||
|
||||
type ArticleContainerProps = {
|
||||
itemId: number
|
||||
@ -58,6 +59,14 @@ const mapDispatchToProps = (dispatch: AppDispatch) => {
|
||||
imageMenu: (position: [number, number]) =>
|
||||
dispatch(openImageMenu(position)),
|
||||
dismissContextMenu: () => dispatch(closeContextMenu()),
|
||||
updateSourceTextDirection: (
|
||||
source: RSSSource,
|
||||
direction: SourceTextDirection
|
||||
) => {
|
||||
dispatch(
|
||||
updateSource({ ...source, textDir: direction } as RSSSource)
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,6 @@ import {
|
||||
app,
|
||||
session,
|
||||
clipboard,
|
||||
TouchBar,
|
||||
} from "electron"
|
||||
import { WindowManager } from "./window"
|
||||
import fs = require("fs")
|
||||
@ -28,11 +27,12 @@ export function setUtilsListeners(manager: WindowManager) {
|
||||
}
|
||||
|
||||
app.on("web-contents-created", (_, contents) => {
|
||||
// TODO: Use contents.setWindowOpenHandler instead of new-window listener
|
||||
contents.on("new-window", (event, url, _, disposition) => {
|
||||
if (manager.hasWindow()) event.preventDefault()
|
||||
contents.setWindowOpenHandler((details) => {
|
||||
if (contents.getType() === "webview")
|
||||
openExternal(url, disposition === "background-tab")
|
||||
openExternal(details.url, details.disposition === "background-tab")
|
||||
return {
|
||||
action: manager.hasWindow() ? "deny" : "allow",
|
||||
}
|
||||
})
|
||||
contents.on("will-navigate", (event, url) => {
|
||||
event.preventDefault()
|
||||
|
@ -69,6 +69,7 @@ export class WindowManager {
|
||||
app.getAppPath(),
|
||||
(app.isPackaged ? "dist/" : "") + "preload.js"
|
||||
),
|
||||
nativeWindowOpen: false,
|
||||
},
|
||||
})
|
||||
this.mainWindowState.manage(this.mainWindow)
|
||||
|
@ -4,7 +4,7 @@ import lf from "lovefield"
|
||||
import { RSSSource } from "./models/source"
|
||||
import { RSSItem } from "./models/item"
|
||||
|
||||
const sdbSchema = lf.schema.create("sourcesDB", 1)
|
||||
const sdbSchema = lf.schema.create("sourcesDB", 2)
|
||||
sdbSchema
|
||||
.createTable("sources")
|
||||
.addColumn("sid", lf.Type.INTEGER)
|
||||
@ -17,6 +17,7 @@ sdbSchema
|
||||
.addColumn("serviceRef", lf.Type.STRING)
|
||||
.addColumn("fetchFrequency", lf.Type.NUMBER)
|
||||
.addColumn("rules", lf.Type.OBJECT)
|
||||
.addColumn("textDir", lf.Type.NUMBER)
|
||||
.addNullable(["iconurl", "serviceRef", "rules"])
|
||||
.addIndex("idxURL", ["url"], true)
|
||||
|
||||
@ -48,8 +49,15 @@ export let sources: lf.schema.Table
|
||||
export let itemsDB: lf.Database
|
||||
export let items: lf.schema.Table
|
||||
|
||||
async function onUpgradeSourceDB(rawDb: lf.raw.BackStore) {
|
||||
const version = rawDb.getVersion()
|
||||
if (version < 2) {
|
||||
await rawDb.addTableColumn("sources", "textDir", 0)
|
||||
}
|
||||
}
|
||||
|
||||
export async function init() {
|
||||
sourcesDB = await sdbSchema.connect()
|
||||
sourcesDB = await sdbSchema.connect({ onUpgrade: onUpgradeSourceDB })
|
||||
sources = sourcesDB.getSchema().table("sources")
|
||||
itemsDB = await idbSchema.connect()
|
||||
items = itemsDB.getSchema().table("items")
|
||||
@ -90,6 +98,7 @@ async function migrateNeDB() {
|
||||
// @ts-ignore
|
||||
delete doc._id
|
||||
if (!doc.fetchFrequency) doc.fetchFrequency = 0
|
||||
doc.textDir = 0
|
||||
return sources.createRow(doc)
|
||||
})
|
||||
const iRows = itemDocs.map(doc => {
|
||||
|
@ -66,7 +66,11 @@
|
||||
"loadWebpage": "Load webpage",
|
||||
"loadFull": "Load full content",
|
||||
"notify": "Notify if fetched in background",
|
||||
"dontNotify": "Don't notify"
|
||||
"dontNotify": "Don't notify",
|
||||
"textDir": "Text direction",
|
||||
"LTR": "Left-to-right",
|
||||
"RTL": "Right-to-left",
|
||||
"Vertical": "Vertical"
|
||||
},
|
||||
"context": {
|
||||
"share": "Share",
|
||||
|
@ -66,7 +66,11 @@
|
||||
"loadWebpage": "加载网页",
|
||||
"loadFull": "抓取全文",
|
||||
"notify": "后台抓取时发送通知",
|
||||
"dontNotify": "不发送通知"
|
||||
"dontNotify": "不发送通知",
|
||||
"textDir": "文本方向",
|
||||
"LTR": "从左到右",
|
||||
"RTL": "从右到左",
|
||||
"Vertical": "纵书"
|
||||
},
|
||||
"context": {
|
||||
"share": "分享",
|
||||
|
@ -66,7 +66,11 @@
|
||||
"loadWebpage": "載入網頁",
|
||||
"loadFull": "抓取全文",
|
||||
"notify": "後臺抓取時傳送通知",
|
||||
"dontNotify": "不傳送通知"
|
||||
"dontNotify": "不傳送通知",
|
||||
"textDir": "文本方向",
|
||||
"LTR": "從左到右",
|
||||
"RTL": "從右到左",
|
||||
"Vertical": "縱書"
|
||||
},
|
||||
"context": {
|
||||
"share": "分享",
|
||||
|
@ -23,6 +23,12 @@ export const enum SourceOpenTarget {
|
||||
FullContent,
|
||||
}
|
||||
|
||||
export const enum SourceTextDirection {
|
||||
LTR,
|
||||
RTL,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
export class RSSSource {
|
||||
sid: number
|
||||
url: string
|
||||
@ -34,6 +40,7 @@ export class RSSSource {
|
||||
serviceRef?: string
|
||||
fetchFrequency: number // in minutes
|
||||
rules?: SourceRule[]
|
||||
textDir: SourceTextDirection
|
||||
|
||||
constructor(url: string, name: string = null) {
|
||||
this.url = url
|
||||
@ -41,6 +48,7 @@ export class RSSSource {
|
||||
this.openTarget = SourceOpenTarget.Local
|
||||
this.lastFetched = new Date()
|
||||
this.fetchFrequency = 0
|
||||
this.textDir = SourceTextDirection.LTR
|
||||
}
|
||||
|
||||
static async fetchMetaData(source: RSSSource) {
|
||||
|
@ -3,6 +3,7 @@ import { IPartialTheme, loadTheme } from "@fluentui/react"
|
||||
import locales from "./i18n/_locales"
|
||||
import { ThemeSettings } from "../schema-types"
|
||||
import intl from "react-intl-universal"
|
||||
import { SourceTextDirection } from "./models/source"
|
||||
|
||||
const lightTheme: IPartialTheme = {
|
||||
defaultFontStyle: {
|
||||
@ -119,6 +120,7 @@ export async function importAll() {
|
||||
} else {
|
||||
const sRows = configs.lovefield.sources.map(s => {
|
||||
s.lastFetched = new Date(s.lastFetched)
|
||||
if (!s.textDir) s.textDir = SourceTextDirection.LTR
|
||||
return db.sources.createRow(s)
|
||||
})
|
||||
const iRows = configs.lovefield.items.map(i => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user