add code formatter
This commit is contained in:
parent
0c479f8ddb
commit
6289ef4dd3
|
@ -9,4 +9,8 @@ community_bridge: # Replace with a single Community Bridge project-name e.g., cl
|
|||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: ["https://www.paypal.me/yang991178", "https://hyliu.me/fluent-reader/imgs/alipay.jpg"]
|
||||
custom:
|
||||
[
|
||||
"https://www.paypal.me/yang991178",
|
||||
"https://hyliu.me/fluent-reader/imgs/alipay.jpg",
|
||||
]
|
||||
|
|
|
@ -1,39 +1,39 @@
|
|||
name: CI/CD Release Linux
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
jobs:
|
||||
release-linux:
|
||||
runs-on: ubuntu-latest
|
||||
release-linux:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Build and package the app
|
||||
run: |
|
||||
sudo npm install --unsafe-perm=true --allow-root
|
||||
npm run build
|
||||
sudo npm run package-linux
|
||||
- name: Build and package the app
|
||||
run: |
|
||||
sudo npm install --unsafe-perm=true --allow-root
|
||||
npm run build
|
||||
sudo npm run package-linux
|
||||
|
||||
- name: Get app version
|
||||
id: package-version
|
||||
uses: martinbeentjes/npm-get-version-action@master
|
||||
- name: Get app version
|
||||
id: package-version
|
||||
uses: martinbeentjes/npm-get-version-action@master
|
||||
|
||||
- name: Get release
|
||||
id: get_release
|
||||
uses: bruceadams/get-release@v1.2.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Get release
|
||||
id: get_release
|
||||
uses: bruceadams/get-release@v1.2.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload AppImage to release assets
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.get_release.outputs.upload_url }}
|
||||
asset_path: ./bin/linux/x64/Fluent Reader-${{ steps.package-version.outputs.current-version }}.AppImage
|
||||
asset_name: Fluent.Reader.${{ steps.package-version.outputs.current-version }}.AppImage
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload AppImage to release assets
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.get_release.outputs.upload_url }}
|
||||
asset_path: ./bin/linux/x64/Fluent Reader-${{ steps.package-version.outputs.current-version }}.AppImage
|
||||
asset_name: Fluent.Reader.${{ steps.package-version.outputs.current-version }}.AppImage
|
||||
asset_content_type: application/octet-stream
|
||||
|
|
|
@ -1,77 +1,77 @@
|
|||
name: CI/CD Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: windows-latest
|
||||
release:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Build and package the app
|
||||
run: |
|
||||
npm install
|
||||
npm run build
|
||||
npm run package-win-ci
|
||||
- name: Build and package the app
|
||||
run: |
|
||||
npm install
|
||||
npm run build
|
||||
npm run package-win-ci
|
||||
|
||||
- name: Get app version
|
||||
id: package-version
|
||||
run: |
|
||||
PACKAGE_VERSION=$(cat package.json | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[",]//g' | tr -d '[[:space:]]')
|
||||
echo ::set-output name=current-version::$PACKAGE_VERSION
|
||||
shell: bash
|
||||
- name: Get app version
|
||||
id: package-version
|
||||
run: |
|
||||
PACKAGE_VERSION=$(cat package.json | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[",]//g' | tr -d '[[:space:]]')
|
||||
echo ::set-output name=current-version::$PACKAGE_VERSION
|
||||
shell: bash
|
||||
|
||||
- name: Create release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: Fluent Reader v${{ steps.package-version.outputs.current-version }}
|
||||
draft: true
|
||||
prerelease: false
|
||||
|
||||
- name: Upload x64 exe to release assets
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./bin/win32/x64/Fluent Reader Setup ${{ steps.package-version.outputs.current-version }}.exe
|
||||
asset_name: Fluent.Reader.Setup.${{ steps.package-version.outputs.current-version }}.x64.exe
|
||||
asset_content_type: application/vnd.microsoft.portable-executable
|
||||
- name: Create release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: Fluent Reader v${{ steps.package-version.outputs.current-version }}
|
||||
draft: true
|
||||
prerelease: false
|
||||
|
||||
- name: Upload x86 exe to release assets
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./bin/win32/ia32/Fluent Reader Setup ${{ steps.package-version.outputs.current-version }}.exe
|
||||
asset_name: Fluent.Reader.Setup.${{ steps.package-version.outputs.current-version }}.x86.exe
|
||||
asset_content_type: application/vnd.microsoft.portable-executable
|
||||
- name: Upload x64 exe to release assets
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./bin/win32/x64/Fluent Reader Setup ${{ steps.package-version.outputs.current-version }}.exe
|
||||
asset_name: Fluent.Reader.Setup.${{ steps.package-version.outputs.current-version }}.x64.exe
|
||||
asset_content_type: application/vnd.microsoft.portable-executable
|
||||
|
||||
- name: Upload x64 zip to release assets
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./bin/win32/x64/Fluent Reader-${{ steps.package-version.outputs.current-version }}-win.zip
|
||||
asset_name: Fluent.Reader.Unpacked.${{ steps.package-version.outputs.current-version }}.x64.zip
|
||||
asset_content_type: application/zip
|
||||
- name: Upload x86 exe to release assets
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./bin/win32/ia32/Fluent Reader Setup ${{ steps.package-version.outputs.current-version }}.exe
|
||||
asset_name: Fluent.Reader.Setup.${{ steps.package-version.outputs.current-version }}.x86.exe
|
||||
asset_content_type: application/vnd.microsoft.portable-executable
|
||||
|
||||
- name: Upload x86 zip to release assets
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./bin/win32/ia32/Fluent Reader-${{ steps.package-version.outputs.current-version }}-ia32-win.zip
|
||||
asset_name: Fluent.Reader.Unpacked.${{ steps.package-version.outputs.current-version }}.x86.zip
|
||||
asset_content_type: application/zip
|
||||
- name: Upload x64 zip to release assets
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./bin/win32/x64/Fluent Reader-${{ steps.package-version.outputs.current-version }}-win.zip
|
||||
asset_name: Fluent.Reader.Unpacked.${{ steps.package-version.outputs.current-version }}.x64.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Upload x86 zip to release assets
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./bin/win32/ia32/Fluent Reader-${{ steps.package-version.outputs.current-version }}-ia32-win.zip
|
||||
asset_name: Fluent.Reader.Unpacked.${{ steps.package-version.outputs.current-version }}.x86.zip
|
||||
asset_content_type: application/zip
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
node_modules
|
||||
dist/**/*.js
|
||||
dist/**/*.js.map
|
||||
bin/*
|
||||
.DS_Store
|
||||
*.provisionprofile
|
||||
*.lock
|
||||
|
||||
*.html
|
||||
*.md
|
||||
*.json
|
||||
!src/**/*.json
|
|
@ -0,0 +1,5 @@
|
|||
tabWidth: 4
|
||||
semi: false
|
||||
jsxBracketSameLine: true
|
||||
arrowParens: "avoid"
|
||||
quoteProps: "consistent"
|
|
@ -1,7 +1,9 @@
|
|||
@import "../styles/scroll.css";
|
||||
|
||||
html, body {
|
||||
font-family: "Segoe UI", "Source Han Sans SC Regular", "Microsoft YaHei", sans-serif;
|
||||
html,
|
||||
body {
|
||||
font-family: "Segoe UI", "Source Han Sans SC Regular", "Microsoft YaHei",
|
||||
sans-serif;
|
||||
}
|
||||
html {
|
||||
overflow: hidden scroll;
|
||||
|
@ -21,14 +23,22 @@ html {
|
|||
}
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6, b, strong {
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
b,
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
a {
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover, a:active {
|
||||
a:hover,
|
||||
a:active {
|
||||
color: var(--primary-alt);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
@ -64,7 +74,7 @@ a:hover, a:active {
|
|||
}
|
||||
#main > p.date {
|
||||
color: var(--gray);
|
||||
font-size: .875rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
article {
|
||||
|
@ -81,7 +91,7 @@ article figure {
|
|||
text-align: center;
|
||||
}
|
||||
article figure figcaption {
|
||||
font-size: .875rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--gray);
|
||||
-webkit-user-modify: read-only;
|
||||
}
|
||||
|
@ -90,11 +100,11 @@ article iframe {
|
|||
}
|
||||
article code {
|
||||
font-family: Monaco, Consolas, monospace;
|
||||
font-size: .875rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1;
|
||||
}
|
||||
article blockquote {
|
||||
border-left: 2px solid var(--gray);
|
||||
margin: 1em 0;
|
||||
padding: 0 40px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,8 @@
|
|||
font-size: 12px;
|
||||
}
|
||||
|
||||
.read-indicator, .starred-indicator {
|
||||
.read-indicator,
|
||||
.starred-indicator {
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
@ -99,14 +100,15 @@
|
|||
background-color: var(--white);
|
||||
box-shadow: #0004 0 5px 20px;
|
||||
margin: 18px 12px;
|
||||
transition: box-shadow linear .08s, transform linear .08s;
|
||||
transition: box-shadow linear 0.08s, transform linear 0.08s;
|
||||
animation-fill-mode: none;
|
||||
}
|
||||
.default-card:hover, .ms-Fabric--isFocusVisible .default-card:focus {
|
||||
.default-card:hover,
|
||||
.ms-Fabric--isFocusVisible .default-card:focus {
|
||||
box-shadow: #0006 0 5px 40px;
|
||||
}
|
||||
.default-card:active {
|
||||
transform: scale(.97);
|
||||
transform: scale(0.97);
|
||||
box-shadow: #0004 0 5px 20px;
|
||||
}
|
||||
|
||||
|
@ -132,10 +134,14 @@
|
|||
height: 144px;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
.default-card img.head, .default-card p, .default-card h3 {
|
||||
transition: transform ease-out .12s;
|
||||
.default-card img.head,
|
||||
.default-card p,
|
||||
.default-card h3 {
|
||||
transition: transform ease-out 0.12s;
|
||||
}
|
||||
.default-card.transform:hover img.head, .default-card.transform:hover p, .default-card.transform:hover h3,
|
||||
.default-card.transform:hover img.head,
|
||||
.default-card.transform:hover p,
|
||||
.default-card.transform:hover h3,
|
||||
.ms-Fabric--isFocusVisible .default-card.transform:focus img.head,
|
||||
.ms-Fabric--isFocusVisible .default-card.transform:focus p,
|
||||
.ms-Fabric--isFocusVisible .default-card.transform:focus h3 {
|
||||
|
@ -172,11 +178,12 @@
|
|||
|
||||
.list-card {
|
||||
display: flex;
|
||||
transition: box-shadow linear .08s;
|
||||
transition: box-shadow linear 0.08s;
|
||||
border-bottom: 1px solid var(--neutralQuaternaryAlt);
|
||||
box-shadow: #0000 0 5px 15px;
|
||||
}
|
||||
.list-card:hover, .ms-Fabric--isFocusVisible .list-card:focus {
|
||||
.list-card:hover,
|
||||
.ms-Fabric--isFocusVisible .list-card:focus {
|
||||
box-shadow: #0004 0 5px 15px;
|
||||
}
|
||||
.list-card:active {
|
||||
|
@ -230,7 +237,8 @@
|
|||
height: 100%;
|
||||
border-left: 2px solid var(--primary);
|
||||
}
|
||||
.list-card.read, .list-card.read p.snippet {
|
||||
.list-card.read,
|
||||
.list-card.read p.snippet {
|
||||
color: var(--neutralSecondaryAlt);
|
||||
}
|
||||
|
||||
|
@ -239,20 +247,22 @@
|
|||
padding: 24px;
|
||||
max-height: 160px;
|
||||
display: flex;
|
||||
transition: box-shadow linear .08s, background-color linear .08s, transform linear .08s;
|
||||
transition: box-shadow linear 0.08s, background-color linear 0.08s,
|
||||
transform linear 0.08s;
|
||||
border-bottom: 1px solid var(--neutralQuaternaryAlt);
|
||||
box-shadow: #0000 0 5px 20px;
|
||||
}
|
||||
.magazine-card.read {
|
||||
color: var(--neutralSecondaryAlt);
|
||||
}
|
||||
.magazine-card:hover, .ms-Fabric--isFocusVisible .magazine-card:focus {
|
||||
.magazine-card:hover,
|
||||
.ms-Fabric--isFocusVisible .magazine-card:focus {
|
||||
box-shadow: #0004 0 5px 20px;
|
||||
background-color: var(--white);
|
||||
}
|
||||
.magazine-card:active {
|
||||
box-shadow: #0000 0 5px 20px;
|
||||
transform: scale(.97);
|
||||
transform: scale(0.97);
|
||||
background-color: unset;
|
||||
}
|
||||
.magazine-card div.head {
|
||||
|
@ -279,7 +289,8 @@
|
|||
height: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
.magazine-card h3.title, .magazine-card p.snippet {
|
||||
.magazine-card h3.title,
|
||||
.magazine-card p.snippet {
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
|
@ -304,9 +315,10 @@
|
|||
font-size: 14px;
|
||||
line-height: 31px;
|
||||
padding: 0 9px;
|
||||
transition: box-shadow linear .08s, background-color linear .08s;
|
||||
transition: box-shadow linear 0.08s, background-color linear 0.08s;
|
||||
}
|
||||
.compact-card:hover, .ms-Fabric--isFocusVisible .compact-card:focus {
|
||||
.compact-card:hover,
|
||||
.ms-Fabric--isFocusVisible .compact-card:focus {
|
||||
box-shadow: #0004 0 0 10px;
|
||||
background-color: var(--white);
|
||||
}
|
||||
|
@ -346,4 +358,4 @@
|
|||
}
|
||||
.compact-card .time {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,10 +2,12 @@
|
|||
.ms-Button--commandBar.active .ms-Button-icon {
|
||||
color: #c7e0f4;
|
||||
}
|
||||
.btn-group .btn:hover, .ms-Nav-compositeLink:hover {
|
||||
.btn-group .btn:hover,
|
||||
.ms-Nav-compositeLink:hover {
|
||||
background-color: #fff1;
|
||||
}
|
||||
.btn-group .btn:active, .ms-Nav-compositeLink:active {
|
||||
.btn-group .btn:active,
|
||||
.ms-Nav-compositeLink:active {
|
||||
background-color: #fff2;
|
||||
}
|
||||
.settings .loading {
|
||||
|
@ -14,28 +16,32 @@
|
|||
.default-card {
|
||||
box-shadow: #0006 0px 5px 20px;
|
||||
}
|
||||
.default-card:hover, .ms-Fabric--isFocusVisible .default-card:focus {
|
||||
.default-card:hover,
|
||||
.ms-Fabric--isFocusVisible .default-card:focus {
|
||||
box-shadow: #0008 0px 5px 40px;
|
||||
}
|
||||
.default-card div.bg {
|
||||
background-color: #000b;
|
||||
}
|
||||
.list-card:hover, .ms-Fabric--isFocusVisible .list-card:focus {
|
||||
.list-card:hover,
|
||||
.ms-Fabric--isFocusVisible .list-card:focus {
|
||||
box-shadow: #0006 0px 5px 15px;
|
||||
}
|
||||
.list-card:active {
|
||||
box-shadow: #0000 0px 5px 15px, inset #0006 0px 0px 15px;
|
||||
}
|
||||
.magazine-card:hover, .ms-Fabric--isFocusVisible .magazine-card:focus {
|
||||
.magazine-card:hover,
|
||||
.ms-Fabric--isFocusVisible .magazine-card:focus {
|
||||
box-shadow: #0006 0px 5px 20px;
|
||||
}
|
||||
.magazine-card:active {
|
||||
box-shadow: #0000 0px 5px 20px;
|
||||
}
|
||||
.compact-card:hover, .ms-Fabric--isFocusVisible .compact-card:focus {
|
||||
.compact-card:hover,
|
||||
.ms-Fabric--isFocusVisible .compact-card:focus {
|
||||
box-shadow: #0008 0 0 10px;
|
||||
}
|
||||
.compact-card:active {
|
||||
box-shadow: #0000 0 0 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
@keyframes slideUp20 {
|
||||
0% { transform: translateY(20px); }
|
||||
100% { transform: translateY(0); }
|
||||
0% {
|
||||
transform: translateY(20px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
.article-wrapper {
|
||||
margin: 32px auto 0;
|
||||
width: 860px;
|
||||
height: calc(100% - 50px);
|
||||
background-color: var(--white);
|
||||
box-shadow: 0 6.4px 14.4px 0 rgba(0,0,0,.132), 0 1.2px 3.6px 0 rgba(0,0,0,.108);
|
||||
box-shadow: 0 6.4px 14.4px 0 rgba(0, 0, 0, 0.132),
|
||||
0 1.2px 3.6px 0 rgba(0, 0, 0, 0.108);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
animation-name: slideUp20;
|
||||
|
@ -17,7 +22,7 @@
|
|||
}
|
||||
.article-container .btn-group {
|
||||
position: absolute;
|
||||
top: calc(50% - 32px)
|
||||
top: calc(50% - 32px);
|
||||
}
|
||||
.article-container .btn-group.prev {
|
||||
left: calc(50% - 486px);
|
||||
|
@ -29,7 +34,8 @@
|
|||
height: 100%;
|
||||
user-select: none;
|
||||
}
|
||||
.article webview, .article .error-prompt {
|
||||
.article webview,
|
||||
.article .error-prompt {
|
||||
width: 100%;
|
||||
height: calc(100% - 36px);
|
||||
border: none;
|
||||
|
@ -45,7 +51,8 @@
|
|||
color: var(--black);
|
||||
border-bottom: 1px solid var(--neutralQuaternaryAlt);
|
||||
}
|
||||
.article .actions .favicon, .article .actions .ms-Spinner {
|
||||
.article .actions .favicon,
|
||||
.article .actions .ms-Spinner {
|
||||
margin: 8px 8px 11px 0;
|
||||
}
|
||||
.article .actions .ms-Spinner {
|
||||
|
@ -70,7 +77,8 @@
|
|||
content: "/";
|
||||
margin: 0 6px;
|
||||
}
|
||||
.side-article-wrapper, .side-logo-wrapper {
|
||||
.side-article-wrapper,
|
||||
.side-logo-wrapper {
|
||||
flex-grow: 1;
|
||||
padding-top: var(--navHeight);
|
||||
height: calc(100% - var(--navHeight));
|
||||
|
@ -108,7 +116,8 @@
|
|||
.side-article-wrapper .article > .ms-Stack {
|
||||
border-top: 1px solid var(--neutralQuaternaryAlt);
|
||||
}
|
||||
.list-feed-container:first-child::before, .side-article-wrapper::before {
|
||||
.list-feed-container:first-child::before,
|
||||
.side-article-wrapper::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
@ -156,7 +165,8 @@
|
|||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.magazine-feed, .compact-feed {
|
||||
.magazine-feed,
|
||||
.compact-feed {
|
||||
padding-top: 28px;
|
||||
height: calc(100% - 60px);
|
||||
overflow: hidden scroll;
|
||||
|
@ -184,7 +194,8 @@
|
|||
justify-content: space-around;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.cards-feed-container > div.load-more-wrapper, .flex-fix {
|
||||
.cards-feed-container > div.load-more-wrapper,
|
||||
.flex-fix {
|
||||
text-align: center;
|
||||
}
|
||||
.cards-feed-container > div.load-more-wrapper {
|
||||
|
@ -206,4 +217,4 @@
|
|||
color: var(--neutralSecondary);
|
||||
font-size: 14px;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,14 +46,17 @@ body.darwin {
|
|||
--navHeight: 38px;
|
||||
}
|
||||
|
||||
html, body {
|
||||
html,
|
||||
body {
|
||||
background-color: transparent;
|
||||
font-family: "Segoe UI", "Source Han Sans SC Regular", "Microsoft YaHei", sans-serif;
|
||||
font-family: "Segoe UI", "Source Han Sans SC Regular", "Microsoft YaHei",
|
||||
sans-serif;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
}
|
||||
body.win32, body.linux {
|
||||
body.win32,
|
||||
body.linux {
|
||||
background-color: var(--neutralLighterAlt);
|
||||
}
|
||||
#root {
|
||||
|
@ -63,12 +66,15 @@ body.win32, body.linux {
|
|||
.ms-Link {
|
||||
user-select: none;
|
||||
}
|
||||
.ms-ContextualMenu-link, .ms-Button, .ms-ContextualMenu-item button {
|
||||
.ms-ContextualMenu-link,
|
||||
.ms-Button,
|
||||
.ms-ContextualMenu-item button {
|
||||
cursor: default;
|
||||
font-size: 13px;
|
||||
user-select: none;
|
||||
}
|
||||
.ms-Nav-link, .ms-Nav-chevronButton {
|
||||
.ms-Nav-link,
|
||||
.ms-Nav-chevronButton {
|
||||
font-size: 12px;
|
||||
line-height: 32px;
|
||||
height: 32px;
|
||||
|
@ -105,13 +111,16 @@ i.ms-Nav-chevron {
|
|||
.ms-Nav-groupContent {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.ms-ActivityItem-activityTypeIcon, .ms-ActivityItem-timeStamp {
|
||||
.ms-ActivityItem-activityTypeIcon,
|
||||
.ms-ActivityItem-timeStamp {
|
||||
user-select: none;
|
||||
}
|
||||
.ms-Label, .ms-Spinner-label {
|
||||
.ms-Label,
|
||||
.ms-Spinner-label {
|
||||
user-select: none;
|
||||
}
|
||||
.ms-ActivityItem, .ms-ActivityItem-commentText {
|
||||
.ms-ActivityItem,
|
||||
.ms-ActivityItem-commentText {
|
||||
color: var(--neutralSecondary);
|
||||
}
|
||||
.ms-ActivityItem-timeStamp {
|
||||
|
@ -135,7 +144,8 @@ i.ms-Nav-chevron {
|
|||
user-select: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
#root > nav .btn, #root > nav span {
|
||||
#root > nav .btn,
|
||||
#root > nav span {
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
@ -203,7 +213,8 @@ body.darwin .btn-group .seperator {
|
|||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
}
|
||||
#root > nav .btn-group .btn, .menu .btn-group .btn {
|
||||
#root > nav .btn-group .btn,
|
||||
.menu .btn-group .btn {
|
||||
height: var(--navHeight);
|
||||
line-height: var(--navHeight);
|
||||
}
|
||||
|
@ -223,16 +234,19 @@ nav.hide-btns .btn-group .btn.system {
|
|||
nav.item-on .btn-group .btn.system {
|
||||
color: var(--whiteConstant);
|
||||
}
|
||||
.btn-group .btn:hover, .ms-Nav-compositeLink:hover {
|
||||
.btn-group .btn:hover,
|
||||
.ms-Nav-compositeLink:hover {
|
||||
background-color: #0001;
|
||||
}
|
||||
.btn-group .btn:active, .ms-Nav-compositeLink:active {
|
||||
.btn-group .btn:active,
|
||||
.ms-Nav-compositeLink:active {
|
||||
background-color: #0002;
|
||||
}
|
||||
.ms-Nav-compositeLink:hover .ms-Nav-link {
|
||||
background: none;
|
||||
}
|
||||
.btn-group .btn.disabled, .btn-group .btn.fetching {
|
||||
.btn-group .btn.disabled,
|
||||
.btn-group .btn.fetching {
|
||||
background-color: unset !important;
|
||||
color: var(--neutralSecondaryAlt);
|
||||
}
|
||||
|
@ -240,8 +254,12 @@ nav.item-on .btn-group .btn.system {
|
|||
animation: rotating linear 1.5s infinite;
|
||||
}
|
||||
@keyframes rotating {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.btn-group .btn.close:hover {
|
||||
background-color: #e81123;
|
||||
|
@ -251,9 +269,9 @@ nav.item-on .btn-group .btn.system {
|
|||
background-color: #f1707a;
|
||||
color: var(--whiteConstant) !important;
|
||||
}
|
||||
.btn-group .btn.inline-block-wide {
|
||||
.btn-group .btn.inline-block-wide {
|
||||
display: none;
|
||||
}
|
||||
body.darwin .btn-group .btn.system {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,10 +6,15 @@
|
|||
}
|
||||
|
||||
@keyframes fade {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.menu-container, .article-container {
|
||||
.menu-container,
|
||||
.article-container {
|
||||
position: fixed;
|
||||
z-index: 5;
|
||||
left: 0;
|
||||
|
@ -22,7 +27,9 @@
|
|||
animation-name: fade;
|
||||
background-color: #0008;
|
||||
}
|
||||
.menu-container, .article-container, .article-wrapper {
|
||||
.menu-container,
|
||||
.article-container,
|
||||
.article-wrapper {
|
||||
animation-duration: 0.5s;
|
||||
animation-timing-function: var(--transition-timing);
|
||||
animation-fill-mode: both;
|
||||
|
@ -45,7 +52,8 @@
|
|||
background-color: var(--neutralLighterAltOpacity);
|
||||
backdrop-filter: var(--blur);
|
||||
box-shadow: 5px 0 25px #0004;
|
||||
transition: clip-path var(--transition-timing) .367s, opacity cubic-bezier(0, 0, 0.2, 1) .367s;
|
||||
transition: clip-path var(--transition-timing) 0.367s,
|
||||
opacity cubic-bezier(0, 0, 0.2, 1) 0.367s;
|
||||
clip-path: inset(0 100% 0 0);
|
||||
opacity: 0;
|
||||
}
|
||||
|
@ -102,7 +110,8 @@ body.darwin .menu .btn-group {
|
|||
width: 680px;
|
||||
height: calc(100% - 64px);
|
||||
background-color: var(--white);
|
||||
box-shadow: 0 6.4px 14.4px 0 rgba(0,0,0,.132), 0 1.2px 3.6px 0 rgba(0,0,0,.108);
|
||||
box-shadow: 0 6.4px 14.4px 0 rgba(0, 0, 0, 0.132),
|
||||
0 1.2px 3.6px 0 rgba(0, 0, 0, 0.108);
|
||||
overflow: hidden;
|
||||
}
|
||||
div[role="toolbar"] {
|
||||
|
@ -212,7 +221,7 @@ img.favicon.dropdown {
|
|||
.article-search {
|
||||
z-index: 4;
|
||||
position: absolute;
|
||||
top:0;
|
||||
top: 0;
|
||||
left: 36px;
|
||||
width: 100%;
|
||||
max-width: calc(100% - 484px);
|
||||
|
@ -220,7 +229,8 @@ img.favicon.dropdown {
|
|||
border: none;
|
||||
-webkit-app-region: none;
|
||||
height: calc(var(--navHeight) - 4px);
|
||||
box-shadow: 0 1.6px 3.6px 0 rgba(0,0,0,.132), 0 0.3px 0.9px 0 rgba(0,0,0,.108);
|
||||
box-shadow: 0 1.6px 3.6px 0 rgba(0, 0, 0, 0.132),
|
||||
0 0.3px 0.9px 0 rgba(0, 0, 0, 0.108);
|
||||
}
|
||||
body.darwin.not-fullscreen .article-search {
|
||||
left: 108px;
|
||||
|
@ -236,8 +246,9 @@ body.darwin .list-main .article-search {
|
|||
top: var(--navHeight);
|
||||
margin: 0 10px;
|
||||
}
|
||||
.main, .list-main {
|
||||
transition: margin-left var(--transition-timing) .367s;
|
||||
.main,
|
||||
.list-main {
|
||||
transition: margin-left var(--transition-timing) 0.367s;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
|
@ -245,7 +256,8 @@ body.darwin .list-main .article-search {
|
|||
#root > nav.menu-on {
|
||||
padding-left: 296px;
|
||||
}
|
||||
#root > nav.menu-on span.title, body.darwin #root > nav.menu-on span.title {
|
||||
#root > nav.menu-on span.title,
|
||||
body.darwin #root > nav.menu-on span.title {
|
||||
max-width: 300px;
|
||||
}
|
||||
nav.menu-on .btn-group .btn {
|
||||
|
@ -283,7 +295,8 @@ body.darwin .list-main .article-search {
|
|||
height: 120%;
|
||||
box-shadow: inset 5px 0 25px #0004;
|
||||
}
|
||||
.main.menu-on, .list-main.menu-on {
|
||||
.main.menu-on,
|
||||
.list-main.menu-on {
|
||||
margin-left: 280px;
|
||||
}
|
||||
.menu-on .article-search {
|
||||
|
@ -303,10 +316,12 @@ body.darwin .list-main .article-search {
|
|||
top: 4px;
|
||||
}
|
||||
|
||||
nav.hide-btns .btn-group .btn, nav.menu-on .btn-group .btn.hide-wide, .menu .btn-group .btn.hide-wide {
|
||||
nav.hide-btns .btn-group .btn,
|
||||
nav.menu-on .btn-group .btn.hide-wide,
|
||||
.menu .btn-group .btn.hide-wide {
|
||||
display: none;
|
||||
}
|
||||
.btn-group .btn.inline-block-wide {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
html, body {
|
||||
html,
|
||||
body {
|
||||
background-color: #f3f2f1;
|
||||
font-family: "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
margin: 0;
|
||||
|
@ -13,13 +14,15 @@ a {
|
|||
color: #0078d4;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover, a:active {
|
||||
a:hover,
|
||||
a:active {
|
||||
color: #004578;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.elevate {
|
||||
box-shadow: 0 6.4px 14.4px 0 rgba(0,0,0,.132), 0 1.2px 3.6px 0 rgba(0,0,0,.108);
|
||||
box-shadow: 0 6.4px 14.4px 0 rgba(0, 0, 0, 0.132),
|
||||
0 1.2px 3.6px 0 rgba(0, 0, 0, 0.108);
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
|
@ -67,14 +70,16 @@ a:hover, a:active {
|
|||
position: relative;
|
||||
top: -280px;
|
||||
}
|
||||
.light-container h1, .dark-container h1 {
|
||||
.light-container h1,
|
||||
.dark-container h1 {
|
||||
width: 95%;
|
||||
max-width: 800px;
|
||||
margin: 48px auto 24px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
}
|
||||
.light-container p, .dark-container p {
|
||||
.light-container p,
|
||||
.dark-container p {
|
||||
width: 85%;
|
||||
max-width: 750px;
|
||||
margin: 24px auto;
|
||||
|
@ -108,7 +113,7 @@ a:hover, a:active {
|
|||
.features-container > section > h3 {
|
||||
font-weight: 500;
|
||||
color: #605e5c;
|
||||
margin: 0 0 .5em;
|
||||
margin: 0 0 0.5em;
|
||||
}
|
||||
.features-container > section > h3 > span {
|
||||
color: #d2d0ce;
|
||||
|
@ -170,13 +175,16 @@ a:hover, a:active {
|
|||
justify-content: center;
|
||||
margin: calc(50vh - 210px) 0 48px;
|
||||
}
|
||||
.links > a {
|
||||
.links > a {
|
||||
display: inline-block;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 780px) {
|
||||
html, body { font-size: 14px; }
|
||||
html,
|
||||
body {
|
||||
font-size: 14px;
|
||||
}
|
||||
.logo-container img {
|
||||
height: 140px;
|
||||
width: 140px;
|
||||
|
|
|
@ -3,33 +3,33 @@ buildVersion: 24
|
|||
productName: Fluent Reader
|
||||
copyright: Copyright © 2020 Haoyuan Liu
|
||||
files:
|
||||
- "./dist/**/*"
|
||||
- "!**/*.js.map"
|
||||
- "./dist/**/*"
|
||||
- "!**/*.js.map"
|
||||
directories:
|
||||
output: "./bin/${platform}/${arch}/"
|
||||
output: "./bin/${platform}/${arch}/"
|
||||
mac:
|
||||
darkModeSupport: true
|
||||
target:
|
||||
- dmg
|
||||
category: public.app-category.news
|
||||
electronLanguages:
|
||||
- zh_CN
|
||||
- zh_TW
|
||||
- en
|
||||
- fr
|
||||
- es
|
||||
- de
|
||||
- tr
|
||||
- ja
|
||||
- sv
|
||||
- uk
|
||||
- it
|
||||
- nl
|
||||
minimumSystemVersion: 10.14.0
|
||||
darkModeSupport: true
|
||||
target:
|
||||
- dmg
|
||||
category: public.app-category.news
|
||||
electronLanguages:
|
||||
- zh_CN
|
||||
- zh_TW
|
||||
- en
|
||||
- fr
|
||||
- es
|
||||
- de
|
||||
- tr
|
||||
- ja
|
||||
- sv
|
||||
- uk
|
||||
- it
|
||||
- nl
|
||||
minimumSystemVersion: 10.14.0
|
||||
mas:
|
||||
entitlements: build/entitlements.mas.plist
|
||||
entitlementsInherit: build/entitlements.mas.inherit.plist
|
||||
provisioningProfile: build/embedded.provisionprofile
|
||||
hardenedRuntime: false
|
||||
gatekeeperAssess: false
|
||||
asarUnpack: []
|
||||
entitlements: build/entitlements.mas.plist
|
||||
entitlementsInherit: build/entitlements.mas.inherit.plist
|
||||
provisioningProfile: build/embedded.provisionprofile
|
||||
hardenedRuntime: false
|
||||
gatekeeperAssess: false
|
||||
asarUnpack: []
|
||||
|
|
|
@ -2,61 +2,61 @@ appId: me.hyliu.fluentreader
|
|||
productName: Fluent Reader
|
||||
copyright: Copyright © 2020 Haoyuan Liu
|
||||
files:
|
||||
- "./dist/**/*"
|
||||
- "!**/*.js.map"
|
||||
- "./dist/**/*"
|
||||
- "!**/*.js.map"
|
||||
directories:
|
||||
output: "./bin/${platform}/${arch}/"
|
||||
output: "./bin/${platform}/${arch}/"
|
||||
mac:
|
||||
darkModeSupport: true
|
||||
target:
|
||||
- dmg
|
||||
category: public.app-category.news
|
||||
electronLanguages:
|
||||
- zh_CN
|
||||
- zh_TW
|
||||
- en
|
||||
- fr
|
||||
- es
|
||||
- de
|
||||
- tr
|
||||
- ja
|
||||
- sv
|
||||
- uk
|
||||
- it
|
||||
- nl
|
||||
darkModeSupport: true
|
||||
target:
|
||||
- dmg
|
||||
category: public.app-category.news
|
||||
electronLanguages:
|
||||
- zh_CN
|
||||
- zh_TW
|
||||
- en
|
||||
- fr
|
||||
- es
|
||||
- de
|
||||
- tr
|
||||
- ja
|
||||
- sv
|
||||
- uk
|
||||
- it
|
||||
- nl
|
||||
win:
|
||||
target:
|
||||
- nsis
|
||||
- zip
|
||||
target:
|
||||
- nsis
|
||||
- zip
|
||||
appx:
|
||||
applicationId: FluentReader
|
||||
identityName: 25286HaoyuanLiu.FluentReader
|
||||
publisher: CN=FD70E7FA-E5AC-41C4-B9C4-6E8708A6616A
|
||||
backgroundColor: transparent
|
||||
languages:
|
||||
- zh-CN
|
||||
- zh-TW
|
||||
- en-US
|
||||
- fr-FR
|
||||
- es
|
||||
- de
|
||||
- tr
|
||||
- ja
|
||||
- sv
|
||||
- uk
|
||||
- it
|
||||
- nl
|
||||
showNameOnTiles: true
|
||||
setBuildNumber: true
|
||||
applicationId: FluentReader
|
||||
identityName: 25286HaoyuanLiu.FluentReader
|
||||
publisher: CN=FD70E7FA-E5AC-41C4-B9C4-6E8708A6616A
|
||||
backgroundColor: transparent
|
||||
languages:
|
||||
- zh-CN
|
||||
- zh-TW
|
||||
- en-US
|
||||
- fr-FR
|
||||
- es
|
||||
- de
|
||||
- tr
|
||||
- ja
|
||||
- sv
|
||||
- uk
|
||||
- it
|
||||
- nl
|
||||
showNameOnTiles: true
|
||||
setBuildNumber: true
|
||||
nsis:
|
||||
oneClick: false
|
||||
perMachine: true
|
||||
allowToChangeInstallationDirectory: true
|
||||
deleteAppDataOnUninstall: true
|
||||
oneClick: false
|
||||
perMachine: true
|
||||
allowToChangeInstallationDirectory: true
|
||||
deleteAppDataOnUninstall: true
|
||||
linux:
|
||||
target:
|
||||
- AppImage
|
||||
icon: build/icons
|
||||
category: Utility
|
||||
desktop:
|
||||
StartupWMClass: fluent-reader
|
||||
target:
|
||||
- AppImage
|
||||
icon: build/icons
|
||||
category: Utility
|
||||
desktop:
|
||||
StartupWMClass: fluent-reader
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -7,6 +7,7 @@
|
|||
"build": "webpack --config ./webpack.config.js",
|
||||
"electron": "electron ./dist/electron.js",
|
||||
"start": "npm run build && npm run electron",
|
||||
"format": "prettier --write .",
|
||||
"package-win": "electron-builder -w appx:x64 && electron-builder -w appx:ia32 && electron-builder -w appx:arm64",
|
||||
"package-win-ci": "electron-builder -w --x64 -p never && electron-builder -w --ia32 -p never",
|
||||
"package-mac": "electron-builder --mac --x64",
|
||||
|
@ -35,6 +36,7 @@
|
|||
"js-md5": "^0.7.3",
|
||||
"lovefield": "^2.1.12",
|
||||
"nedb": "^1.8.0",
|
||||
"prettier": "2.3.2",
|
||||
"qrcode.react": "^1.0.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
|
@ -45,7 +47,7 @@
|
|||
"redux-thunk": "^2.3.0",
|
||||
"reselect": "^4.0.0",
|
||||
"ts-loader": "^7.0.4",
|
||||
"typescript": "^3.9.2",
|
||||
"typescript": "^4.3.5",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-cli": "^3.3.11"
|
||||
}
|
||||
|
|
|
@ -1,4 +1,11 @@
|
|||
import { SourceGroup, ViewType, ThemeSettings, SearchEngines, ServiceConfigs, ViewConfigs } from "../schema-types"
|
||||
import {
|
||||
SourceGroup,
|
||||
ViewType,
|
||||
ThemeSettings,
|
||||
SearchEngines,
|
||||
ServiceConfigs,
|
||||
ViewConfigs,
|
||||
} from "../schema-types"
|
||||
import { ipcRenderer } from "electron"
|
||||
|
||||
const settingsBridge = {
|
||||
|
@ -114,15 +121,15 @@ const settingsBridge = {
|
|||
return ipcRenderer.sendSync("get-all-settings") as Object
|
||||
},
|
||||
|
||||
setAll: (configs) => {
|
||||
setAll: configs => {
|
||||
ipcRenderer.invoke("import-all-settings", configs)
|
||||
},
|
||||
}
|
||||
|
||||
declare global {
|
||||
declare global {
|
||||
interface Window {
|
||||
settings: typeof settingsBridge
|
||||
}
|
||||
}
|
||||
|
||||
export default settingsBridge
|
||||
export default settingsBridge
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import { ipcRenderer } from "electron"
|
||||
import { ImageCallbackTypes, TouchBarTexts, WindowStateListenerType } from "../schema-types"
|
||||
import {
|
||||
ImageCallbackTypes,
|
||||
TouchBarTexts,
|
||||
WindowStateListenerType,
|
||||
} from "../schema-types"
|
||||
import { IObjectWithKey } from "@fluentui/react"
|
||||
|
||||
const utilsBridge = {
|
||||
|
@ -9,7 +13,7 @@ const utilsBridge = {
|
|||
return ipcRenderer.sendSync("get-version")
|
||||
},
|
||||
|
||||
openExternal: (url: string, background=false) => {
|
||||
openExternal: (url: string, background = false) => {
|
||||
ipcRenderer.invoke("open-external", url, background)
|
||||
},
|
||||
|
||||
|
@ -17,12 +21,31 @@ const utilsBridge = {
|
|||
ipcRenderer.invoke("show-error-box", title, content)
|
||||
},
|
||||
|
||||
showMessageBox: async (title: string, message: string, confirm: string, cancel: string, defaultCancel=false, type="none") => {
|
||||
return await ipcRenderer.invoke("show-message-box", title, message, confirm, cancel, defaultCancel, type) as boolean
|
||||
showMessageBox: async (
|
||||
title: string,
|
||||
message: string,
|
||||
confirm: string,
|
||||
cancel: string,
|
||||
defaultCancel = false,
|
||||
type = "none"
|
||||
) => {
|
||||
return (await ipcRenderer.invoke(
|
||||
"show-message-box",
|
||||
title,
|
||||
message,
|
||||
confirm,
|
||||
cancel,
|
||||
defaultCancel,
|
||||
type
|
||||
)) as boolean
|
||||
},
|
||||
|
||||
showSaveDialog: async (filters: Electron.FileFilter[], path: string) => {
|
||||
let result = await ipcRenderer.invoke("show-save-dialog", filters, path) as boolean
|
||||
let result = (await ipcRenderer.invoke(
|
||||
"show-save-dialog",
|
||||
filters,
|
||||
path
|
||||
)) as boolean
|
||||
if (result) {
|
||||
return (result: string, errmsg: string) => {
|
||||
ipcRenderer.invoke("write-save-result", result, errmsg)
|
||||
|
@ -33,7 +56,7 @@ const utilsBridge = {
|
|||
},
|
||||
|
||||
showOpenDialog: async (filters: Electron.FileFilter[]) => {
|
||||
return await ipcRenderer.invoke("show-open-dialog", filters) as string
|
||||
return (await ipcRenderer.invoke("show-open-dialog", filters)) as string
|
||||
},
|
||||
|
||||
getCacheSize: async (): Promise<number> => {
|
||||
|
@ -44,13 +67,17 @@ const utilsBridge = {
|
|||
await ipcRenderer.invoke("clear-cache")
|
||||
},
|
||||
|
||||
addMainContextListener: (callback: (pos: [number, number], text: string) => any) => {
|
||||
addMainContextListener: (
|
||||
callback: (pos: [number, number], text: string) => any
|
||||
) => {
|
||||
ipcRenderer.removeAllListeners("window-context-menu")
|
||||
ipcRenderer.on("window-context-menu", (_, pos, text) => {
|
||||
callback(pos, text)
|
||||
})
|
||||
},
|
||||
addWebviewContextListener: (callback: (pos: [number, number], text: string, url: string) => any) => {
|
||||
addWebviewContextListener: (
|
||||
callback: (pos: [number, number], text: string, url: string) => any
|
||||
) => {
|
||||
ipcRenderer.removeAllListeners("webview-context-menu")
|
||||
ipcRenderer.on("webview-context-menu", (_, pos, text, url) => {
|
||||
callback(pos, text, url)
|
||||
|
@ -102,7 +129,9 @@ const utilsBridge = {
|
|||
requestAttention: () => {
|
||||
ipcRenderer.invoke("request-attention")
|
||||
},
|
||||
addWindowStateListener: (callback: (type: WindowStateListenerType, state: boolean) => any) => {
|
||||
addWindowStateListener: (
|
||||
callback: (type: WindowStateListenerType, state: boolean) => any
|
||||
) => {
|
||||
ipcRenderer.removeAllListeners("maximized")
|
||||
ipcRenderer.on("maximized", () => {
|
||||
callback(WindowStateListenerType.Maximized, true)
|
||||
|
@ -132,7 +161,7 @@ const utilsBridge = {
|
|||
addTouchBarEventsListener: (callback: (IObjectWithKey) => any) => {
|
||||
ipcRenderer.removeAllListeners("touchbar-event")
|
||||
ipcRenderer.on("touchbar-event", (_, key: string) => {
|
||||
callback({ key: key } )
|
||||
callback({ key: key })
|
||||
})
|
||||
},
|
||||
initTouchBar: (texts: TouchBarTexts) => {
|
||||
|
@ -143,10 +172,10 @@ const utilsBridge = {
|
|||
},
|
||||
}
|
||||
|
||||
declare global {
|
||||
declare global {
|
||||
interface Window {
|
||||
utils: typeof utilsBridge
|
||||
}
|
||||
}
|
||||
|
||||
export default utilsBridge
|
||||
export default utilsBridge
|
||||
|
|
|
@ -2,7 +2,16 @@ import * as React from "react"
|
|||
import intl from "react-intl-universal"
|
||||
import { renderToString } from "react-dom/server"
|
||||
import { RSSItem } from "../scripts/models/item"
|
||||
import { Stack, CommandBarButton, IContextualMenuProps, FocusZone, ContextualMenuItemType, Spinner, Icon, Link } from "@fluentui/react"
|
||||
import {
|
||||
Stack,
|
||||
CommandBarButton,
|
||||
IContextualMenuProps,
|
||||
FocusZone,
|
||||
ContextualMenuItemType,
|
||||
Spinner,
|
||||
Icon,
|
||||
Link,
|
||||
} from "@fluentui/react"
|
||||
import { RSSSource, SourceOpenTarget } from "../scripts/models/source"
|
||||
import { shareSubmenu } from "./context-menu"
|
||||
import { platformCtrl, decodeFetchResponse } from "../scripts/utils"
|
||||
|
@ -51,7 +60,8 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
|||
window.utils.addWebviewContextListener(this.contextMenuHandler)
|
||||
window.utils.addWebviewKeydownListener(this.keyDownHandler)
|
||||
window.utils.addWebviewErrorListener(this.webviewError)
|
||||
if (props.source.openTarget === SourceOpenTarget.FullContent) this.loadFull()
|
||||
if (props.source.openTarget === SourceOpenTarget.FullContent)
|
||||
this.loadFull()
|
||||
}
|
||||
|
||||
getFontSize = () => {
|
||||
|
@ -59,7 +69,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
|||
}
|
||||
setFontSize = (size: number) => {
|
||||
window.settings.setFontSize(size)
|
||||
this.setState({fontSize: size})
|
||||
this.setState({ fontSize: size })
|
||||
}
|
||||
|
||||
fontMenuProps = (): IContextualMenuProps => ({
|
||||
|
@ -68,8 +78,8 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
|||
text: String(size),
|
||||
canCheck: true,
|
||||
checked: size === this.state.fontSize,
|
||||
onClick: () => this.setFontSize(size)
|
||||
}))
|
||||
onClick: () => this.setFontSize(size),
|
||||
})),
|
||||
})
|
||||
|
||||
moreMenuProps = (): IContextualMenuProps => ({
|
||||
|
@ -78,33 +88,46 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
|||
key: "openInBrowser",
|
||||
text: intl.get("openExternal"),
|
||||
iconProps: { iconName: "NavigateExternalInline" },
|
||||
onClick: e => { window.utils.openExternal(this.props.item.link, platformCtrl(e)) }
|
||||
onClick: e => {
|
||||
window.utils.openExternal(
|
||||
this.props.item.link,
|
||||
platformCtrl(e)
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "copyURL",
|
||||
text: intl.get("context.copyURL"),
|
||||
iconProps: { iconName: "Link" },
|
||||
onClick: () => { window.utils.writeClipboard(this.props.item.link) }
|
||||
onClick: () => {
|
||||
window.utils.writeClipboard(this.props.item.link)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "toggleHidden",
|
||||
text: this.props.item.hidden ? intl.get("article.unhide") : intl.get("article.hide"),
|
||||
iconProps: { iconName: this.props.item.hidden ? "View" : "Hide3" },
|
||||
onClick: () => { this.props.toggleHidden(this.props.item) }
|
||||
text: this.props.item.hidden
|
||||
? intl.get("article.unhide")
|
||||
: intl.get("article.hide"),
|
||||
iconProps: {
|
||||
iconName: this.props.item.hidden ? "View" : "Hide3",
|
||||
},
|
||||
onClick: () => {
|
||||
this.props.toggleHidden(this.props.item)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "fontMenu",
|
||||
text: intl.get("article.fontSize"),
|
||||
iconProps: { iconName: "FontSize" },
|
||||
disabled: this.state.loadWebpage,
|
||||
subMenuProps: this.fontMenuProps()
|
||||
subMenuProps: this.fontMenuProps(),
|
||||
},
|
||||
{
|
||||
key: "divider_1",
|
||||
itemType: ContextualMenuItemType.Divider,
|
||||
},
|
||||
...shareSubmenu(this.props.item)
|
||||
]
|
||||
...shareSubmenu(this.props.item),
|
||||
],
|
||||
})
|
||||
|
||||
contextMenuHandler = (pos: [number, number], text: string, url: string) => {
|
||||
|
@ -126,13 +149,16 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
|||
case "ArrowRight":
|
||||
this.props.offsetItem(input.key === "ArrowLeft" ? -1 : 1)
|
||||
break
|
||||
case "l": case "L":
|
||||
case "l":
|
||||
case "L":
|
||||
this.toggleWebpage()
|
||||
break
|
||||
case "w": case "W":
|
||||
case "w":
|
||||
case "W":
|
||||
this.toggleFull()
|
||||
break
|
||||
case "H": case "h":
|
||||
case "H":
|
||||
case "h":
|
||||
if (!input.meta) this.props.toggleHidden(this.props.item)
|
||||
break
|
||||
default:
|
||||
|
@ -144,7 +170,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
|||
ctrlKey: input.control,
|
||||
metaKey: input.meta,
|
||||
repeat: input.isAutoRepeat,
|
||||
bubbles: true
|
||||
bubbles: true,
|
||||
})
|
||||
this.props.shortcuts(this.props.item, keyboardEvent)
|
||||
document.dispatchEvent(keyboardEvent)
|
||||
|
@ -154,14 +180,14 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
|||
}
|
||||
|
||||
webviewLoaded = () => {
|
||||
this.setState({loaded: true})
|
||||
this.setState({ loaded: true })
|
||||
}
|
||||
webviewError = (reason: string) => {
|
||||
this.setState({error: true, errorDescription: reason})
|
||||
this.setState({ error: true, errorDescription: reason })
|
||||
}
|
||||
webviewReload = () => {
|
||||
if (this.webview) {
|
||||
this.setState({loaded: false, error: false})
|
||||
this.setState({ loaded: false, error: false })
|
||||
this.webview.reload()
|
||||
} else if (this.state.loadFull) {
|
||||
this.loadFull()
|
||||
|
@ -174,9 +200,11 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
|||
this.webview = webview
|
||||
if (webview) {
|
||||
webview.focus()
|
||||
this.setState({loaded: false, error: false})
|
||||
this.setState({ loaded: false, error: false })
|
||||
webview.addEventListener("did-stop-loading", this.webviewLoaded)
|
||||
let card = document.querySelector(`#refocus div[data-iid="${this.props.item._id}"]`) as HTMLElement
|
||||
let card = document.querySelector(
|
||||
`#refocus div[data-iid="${this.props.item._id}"]`
|
||||
) as HTMLElement
|
||||
// @ts-ignore
|
||||
if (card) card.scrollIntoViewIfNeeded()
|
||||
}
|
||||
|
@ -185,23 +213,32 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
|||
componentDidUpdate = (prevProps: ArticleProps) => {
|
||||
if (prevProps.item._id != this.props.item._id) {
|
||||
this.setState({
|
||||
loadWebpage: this.props.source.openTarget === SourceOpenTarget.Webpage,
|
||||
loadFull: this.props.source.openTarget === SourceOpenTarget.FullContent,
|
||||
loadWebpage:
|
||||
this.props.source.openTarget === SourceOpenTarget.Webpage,
|
||||
loadFull:
|
||||
this.props.source.openTarget ===
|
||||
SourceOpenTarget.FullContent,
|
||||
})
|
||||
if (this.props.source.openTarget === SourceOpenTarget.FullContent) this.loadFull()
|
||||
if (this.props.source.openTarget === SourceOpenTarget.FullContent)
|
||||
this.loadFull()
|
||||
}
|
||||
this.componentDidMount()
|
||||
}
|
||||
|
||||
componentWillUnmount = () => {
|
||||
let refocus = document.querySelector(`#refocus div[data-iid="${this.props.item._id}"]`) as HTMLElement
|
||||
let refocus = document.querySelector(
|
||||
`#refocus div[data-iid="${this.props.item._id}"]`
|
||||
) as HTMLElement
|
||||
if (refocus) refocus.focus()
|
||||
}
|
||||
|
||||
toggleWebpage = () => {
|
||||
if (this.state.loadWebpage) {
|
||||
this.setState({ loadWebpage: false })
|
||||
} else if (this.props.item.link.startsWith("https://") || this.props.item.link.startsWith("http://")) {
|
||||
} else if (
|
||||
this.props.item.link.startsWith("https://") ||
|
||||
this.props.item.link.startsWith("http://")
|
||||
) {
|
||||
this.setState({ loadWebpage: true, loadFull: false })
|
||||
}
|
||||
}
|
||||
|
@ -209,7 +246,10 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
|||
toggleFull = () => {
|
||||
if (this.state.loadFull) {
|
||||
this.setState({ loadFull: false })
|
||||
} else if (this.props.item.link.startsWith("https://") || this.props.item.link.startsWith("http://")) {
|
||||
} else if (
|
||||
this.props.item.link.startsWith("https://") ||
|
||||
this.props.item.link.startsWith("http://")
|
||||
) {
|
||||
this.setState({ loadFull: true, loadWebpage: false })
|
||||
this.loadFull()
|
||||
}
|
||||
|
@ -222,86 +262,173 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
|||
const html = await decodeFetchResponse(result, true)
|
||||
this.setState({ fullContent: html })
|
||||
} catch {
|
||||
this.setState({ loaded: true, error: true, errorDescription: "MERCURY_PARSER_FAILURE" })
|
||||
this.setState({
|
||||
loaded: true,
|
||||
error: true,
|
||||
errorDescription: "MERCURY_PARSER_FAILURE",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
articleView = () => {
|
||||
const a = encodeURIComponent(this.state.loadFull ? this.state.fullContent : this.props.item.content)
|
||||
const h = encodeURIComponent(renderToString(<>
|
||||
<p className="title">{this.props.item.title}</p>
|
||||
<p className="date">{this.props.item.date.toLocaleString(this.props.locale, {hour12: !this.props.locale.startsWith("zh")})}</p>
|
||||
<article></article>
|
||||
</>))
|
||||
return `article/article.html?a=${a}&h=${h}&s=${this.state.fontSize}&u=${this.props.item.link}&m=${this.state.loadFull?1:0}`
|
||||
const a = encodeURIComponent(
|
||||
this.state.loadFull
|
||||
? this.state.fullContent
|
||||
: this.props.item.content
|
||||
)
|
||||
const h = encodeURIComponent(
|
||||
renderToString(
|
||||
<>
|
||||
<p className="title">{this.props.item.title}</p>
|
||||
<p className="date">
|
||||
{this.props.item.date.toLocaleString(
|
||||
this.props.locale,
|
||||
{ hour12: !this.props.locale.startsWith("zh") }
|
||||
)}
|
||||
</p>
|
||||
<article></article>
|
||||
</>
|
||||
)
|
||||
)
|
||||
return `article/article.html?a=${a}&h=${h}&s=${this.state.fontSize}&u=${
|
||||
this.props.item.link
|
||||
}&m=${this.state.loadFull ? 1 : 0}`
|
||||
}
|
||||
|
||||
render = () => (
|
||||
<FocusZone className="article">
|
||||
<Stack horizontal style={{height: 36}}>
|
||||
<span style={{width: 96}}></span>
|
||||
<Stack className="actions" grow horizontal tokens={{childrenGap: 12}}>
|
||||
<Stack horizontal style={{ height: 36 }}>
|
||||
<span style={{ width: 96 }}></span>
|
||||
<Stack
|
||||
className="actions"
|
||||
grow
|
||||
horizontal
|
||||
tokens={{ childrenGap: 12 }}>
|
||||
<Stack.Item grow>
|
||||
<span className="source-name">
|
||||
{this.state.loaded
|
||||
? (this.props.source.iconurl && <img className="favicon" src={this.props.source.iconurl} />)
|
||||
: <Spinner size={1} />}
|
||||
{this.state.loaded ? (
|
||||
this.props.source.iconurl && (
|
||||
<img
|
||||
className="favicon"
|
||||
src={this.props.source.iconurl}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<Spinner size={1} />
|
||||
)}
|
||||
{this.props.source.name}
|
||||
{this.props.item.creator && <span className="creator">{this.props.item.creator}</span>}
|
||||
{this.props.item.creator && (
|
||||
<span className="creator">
|
||||
{this.props.item.creator}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</Stack.Item>
|
||||
<CommandBarButton
|
||||
title={this.props.item.hasRead ? intl.get("article.markUnread") : intl.get("article.markRead")}
|
||||
iconProps={this.props.item.hasRead
|
||||
? {iconName: "StatusCircleRing"}
|
||||
: {iconName: "RadioBtnOn", style: {fontSize: 14, textAlign: "center"}}}
|
||||
onClick={() => this.props.toggleHasRead(this.props.item)} />
|
||||
title={
|
||||
this.props.item.hasRead
|
||||
? intl.get("article.markUnread")
|
||||
: intl.get("article.markRead")
|
||||
}
|
||||
iconProps={
|
||||
this.props.item.hasRead
|
||||
? { iconName: "StatusCircleRing" }
|
||||
: {
|
||||
iconName: "RadioBtnOn",
|
||||
style: {
|
||||
fontSize: 14,
|
||||
textAlign: "center",
|
||||
},
|
||||
}
|
||||
}
|
||||
onClick={() =>
|
||||
this.props.toggleHasRead(this.props.item)
|
||||
}
|
||||
/>
|
||||
<CommandBarButton
|
||||
title={this.props.item.starred ? intl.get("article.unstar") : intl.get("article.star")}
|
||||
iconProps={{iconName: this.props.item.starred ? "FavoriteStarFill" : "FavoriteStar"}}
|
||||
onClick={() => this.props.toggleStarred(this.props.item)} />
|
||||
title={
|
||||
this.props.item.starred
|
||||
? intl.get("article.unstar")
|
||||
: intl.get("article.star")
|
||||
}
|
||||
iconProps={{
|
||||
iconName: this.props.item.starred
|
||||
? "FavoriteStarFill"
|
||||
: "FavoriteStar",
|
||||
}}
|
||||
onClick={() =>
|
||||
this.props.toggleStarred(this.props.item)
|
||||
}
|
||||
/>
|
||||
<CommandBarButton
|
||||
title={intl.get("article.loadFull")}
|
||||
className={this.state.loadFull ? "active" : ""}
|
||||
iconProps={{iconName: "RawSource"}}
|
||||
onClick={this.toggleFull} />
|
||||
iconProps={{ iconName: "RawSource" }}
|
||||
onClick={this.toggleFull}
|
||||
/>
|
||||
<CommandBarButton
|
||||
title={intl.get("article.loadWebpage")}
|
||||
className={this.state.loadWebpage ? "active" : ""}
|
||||
iconProps={{iconName: "Globe"}}
|
||||
onClick={this.toggleWebpage} />
|
||||
iconProps={{ iconName: "Globe" }}
|
||||
onClick={this.toggleWebpage}
|
||||
/>
|
||||
<CommandBarButton
|
||||
title={intl.get("more")}
|
||||
iconProps={{iconName: "More"}}
|
||||
menuIconProps={{style: {display: "none"}}}
|
||||
menuProps={this.moreMenuProps()} />
|
||||
iconProps={{ iconName: "More" }}
|
||||
menuIconProps={{ style: { display: "none" } }}
|
||||
menuProps={this.moreMenuProps()}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack horizontal horizontalAlign="end" style={{width: 112}}>
|
||||
<Stack horizontal horizontalAlign="end" style={{ width: 112 }}>
|
||||
<CommandBarButton
|
||||
title={intl.get("close")}
|
||||
iconProps={{iconName: "BackToWindow"}}
|
||||
onClick={this.props.dismiss} />
|
||||
iconProps={{ iconName: "BackToWindow" }}
|
||||
onClick={this.props.dismiss}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
{(!this.state.loadFull || this.state.fullContent) && <webview
|
||||
id="article"
|
||||
className={this.state.error ? "error" : ""}
|
||||
key={this.props.item._id + (this.state.loadWebpage ? "_" : "")}
|
||||
src={this.state.loadWebpage ? this.props.item.link : this.articleView()}
|
||||
webpreferences="contextIsolation,disableDialogs,autoplayPolicy=document-user-activation-required"
|
||||
partition={this.state.loadWebpage ? "sandbox" : undefined} />}
|
||||
{(!this.state.loadFull || this.state.fullContent) && (
|
||||
<webview
|
||||
id="article"
|
||||
className={this.state.error ? "error" : ""}
|
||||
key={
|
||||
this.props.item._id +
|
||||
(this.state.loadWebpage ? "_" : "")
|
||||
}
|
||||
src={
|
||||
this.state.loadWebpage
|
||||
? this.props.item.link
|
||||
: this.articleView()
|
||||
}
|
||||
webpreferences="contextIsolation,disableDialogs,autoplayPolicy=document-user-activation-required"
|
||||
partition={this.state.loadWebpage ? "sandbox" : undefined}
|
||||
/>
|
||||
)}
|
||||
{this.state.error && (
|
||||
<Stack className="error-prompt" verticalAlign="center" horizontalAlign="center" tokens={{childrenGap: 12}}>
|
||||
<Icon iconName="HeartBroken" style={{fontSize: 32}} />
|
||||
<Stack horizontal horizontalAlign="center" tokens={{childrenGap: 7}}>
|
||||
<Stack
|
||||
className="error-prompt"
|
||||
verticalAlign="center"
|
||||
horizontalAlign="center"
|
||||
tokens={{ childrenGap: 12 }}>
|
||||
<Icon iconName="HeartBroken" style={{ fontSize: 32 }} />
|
||||
<Stack
|
||||
horizontal
|
||||
horizontalAlign="center"
|
||||
tokens={{ childrenGap: 7 }}>
|
||||
<small>{intl.get("article.error")}</small>
|
||||
<small><Link onClick={this.webviewReload}>{intl.get("article.reload")}</Link></small>
|
||||
<small>
|
||||
<Link onClick={this.webviewReload}>
|
||||
{intl.get("article.reload")}
|
||||
</Link>
|
||||
</small>
|
||||
</Stack>
|
||||
<span style={{fontSize: 11}}>{this.state.errorDescription}</span>
|
||||
<span style={{ fontSize: 11 }}>
|
||||
{this.state.errorDescription}
|
||||
</span>
|
||||
</Stack>
|
||||
)}
|
||||
</FocusZone>
|
||||
)
|
||||
}
|
||||
|
||||
export default Article
|
||||
export default Article
|
||||
|
|
|
@ -61,4 +61,4 @@ export namespace Card {
|
|||
const onKeyDown = (props: Props, e: React.KeyboardEvent) => {
|
||||
props.shortcuts(props.item, e.nativeEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ const className = (props: Card.Props) => {
|
|||
return cn.join(" ")
|
||||
}
|
||||
|
||||
const CompactCard: React.FunctionComponent<Card.Props> = (props) => (
|
||||
const CompactCard: React.FunctionComponent<Card.Props> = props => (
|
||||
<div
|
||||
className={className(props)}
|
||||
{...Card.bindEventsToProps(props)}
|
||||
|
@ -18,11 +18,19 @@ const CompactCard: React.FunctionComponent<Card.Props> = (props) => (
|
|||
data-is-focusable>
|
||||
<CardInfo source={props.source} item={props.item} hideTime />
|
||||
<div className="data">
|
||||
<span className="title"><Highlights text={props.item.title} filter={props.filter} title /></span>
|
||||
<span className="snippet"><Highlights text={props.item.snippet} filter={props.filter} /></span>
|
||||
<span className="title">
|
||||
<Highlights
|
||||
text={props.item.title}
|
||||
filter={props.filter}
|
||||
title
|
||||
/>
|
||||
</span>
|
||||
<span className="snippet">
|
||||
<Highlights text={props.item.snippet} filter={props.filter} />
|
||||
</span>
|
||||
</div>
|
||||
<Time date={props.item.date} />
|
||||
</div>
|
||||
)
|
||||
|
||||
export default CompactCard
|
||||
export default CompactCard
|
||||
|
|
|
@ -10,7 +10,7 @@ const className = (props: Card.Props) => {
|
|||
return cn.join(" ")
|
||||
}
|
||||
|
||||
const DefaultCard: React.FunctionComponent<Card.Props> = (props) => (
|
||||
const DefaultCard: React.FunctionComponent<Card.Props> = props => (
|
||||
<div
|
||||
className={className(props)}
|
||||
{...Card.bindEventsToProps(props)}
|
||||
|
@ -24,11 +24,13 @@ const DefaultCard: React.FunctionComponent<Card.Props> = (props) => (
|
|||
<img className="head" src={props.item.thumb} />
|
||||
) : null}
|
||||
<CardInfo source={props.source} item={props.item} />
|
||||
<h3 className="title"><Highlights text={props.item.title} filter={props.filter} title /></h3>
|
||||
<h3 className="title">
|
||||
<Highlights text={props.item.title} filter={props.filter} title />
|
||||
</h3>
|
||||
<p className={"snippet" + (props.item.thumb ? "" : " show")}>
|
||||
<Highlights text={props.item.snippet} filter={props.filter} />
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default DefaultCard
|
||||
export default DefaultCard
|
||||
|
|
|
@ -8,11 +8,14 @@ type HighlightsProps = {
|
|||
title?: boolean
|
||||
}
|
||||
|
||||
const Highlights: React.FunctionComponent<HighlightsProps> = (props) => {
|
||||
const Highlights: React.FunctionComponent<HighlightsProps> = props => {
|
||||
const spans: [string, boolean][] = new Array()
|
||||
const flags = (props.filter.type & FilterType.CaseInsensitive) ? "ig" : "g"
|
||||
const flags = props.filter.type & FilterType.CaseInsensitive ? "ig" : "g"
|
||||
let regex: RegExp
|
||||
if (props.filter.search === "" || !(regex = validateRegex(props.filter.search, flags))) {
|
||||
if (
|
||||
props.filter.search === "" ||
|
||||
!(regex = validateRegex(props.filter.search, flags))
|
||||
) {
|
||||
if (props.title) spans.push([props.text, false])
|
||||
else spans.push([props.text.substr(0, 325), false])
|
||||
} else if (props.title) {
|
||||
|
@ -22,7 +25,10 @@ const Highlights: React.FunctionComponent<HighlightsProps> = (props) => {
|
|||
match = regex.exec(props.text)
|
||||
if (match) {
|
||||
if (startIndex != match.index) {
|
||||
spans.push([props.text.substring(startIndex, match.index), false])
|
||||
spans.push([
|
||||
props.text.substring(startIndex, match.index),
|
||||
false,
|
||||
])
|
||||
}
|
||||
spans.push([match[0], true])
|
||||
} else {
|
||||
|
@ -33,8 +39,14 @@ const Highlights: React.FunctionComponent<HighlightsProps> = (props) => {
|
|||
const match = regex.exec(props.text)
|
||||
if (match) {
|
||||
if (match.index != 0) {
|
||||
const startIndex = Math.max(match.index - 25, props.text.lastIndexOf(" ", Math.max(match.index - 10, 0)))
|
||||
spans.push([props.text.substring(Math.max(0, startIndex), match.index), false])
|
||||
const startIndex = Math.max(
|
||||
match.index - 25,
|
||||
props.text.lastIndexOf(" ", Math.max(match.index - 10, 0))
|
||||
)
|
||||
spans.push([
|
||||
props.text.substring(Math.max(0, startIndex), match.index),
|
||||
false,
|
||||
])
|
||||
}
|
||||
spans.push([match[0], true])
|
||||
if (regex.lastIndex < props.text.length) {
|
||||
|
@ -45,9 +57,13 @@ const Highlights: React.FunctionComponent<HighlightsProps> = (props) => {
|
|||
}
|
||||
}
|
||||
|
||||
return <>
|
||||
{spans.map(([text, flag]) => flag ? <span className="h">{text}</span> : text)}
|
||||
</>
|
||||
return (
|
||||
<>
|
||||
{spans.map(([text, flag]) =>
|
||||
flag ? <span className="h">{text}</span> : text
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Highlights
|
||||
export default Highlights
|
||||
|
|
|
@ -10,7 +10,7 @@ type CardInfoProps = {
|
|||
showCreator?: boolean
|
||||
}
|
||||
|
||||
const CardInfo: React.FunctionComponent<CardInfoProps> = (props) => (
|
||||
const CardInfo: React.FunctionComponent<CardInfoProps> = props => (
|
||||
<p className="info">
|
||||
{props.source.iconurl ? <img src={props.source.iconurl} /> : null}
|
||||
<span className="name">
|
||||
|
@ -19,10 +19,12 @@ const CardInfo: React.FunctionComponent<CardInfoProps> = (props) => (
|
|||
<span className="creator">{props.item.creator}</span>
|
||||
)}
|
||||
</span>
|
||||
{props.item.starred ? <span className="starred-indicator"></span> : null}
|
||||
{props.item.starred ? (
|
||||
<span className="starred-indicator"></span>
|
||||
) : null}
|
||||
{props.item.hasRead ? null : <span className="read-indicator"></span>}
|
||||
{props.hideTime ? null : <Time date={props.item.date} />}
|
||||
</p>
|
||||
)
|
||||
|
||||
export default CardInfo
|
||||
export default CardInfo
|
||||
|
|
|
@ -8,27 +8,41 @@ const className = (props: Card.Props) => {
|
|||
let cn = ["card", "list-card"]
|
||||
if (props.item.hidden) cn.push("hidden")
|
||||
if (props.selected) cn.push("selected")
|
||||
if ((props.viewConfigs & ViewConfigs.FadeRead) && props.item.hasRead) cn.push("read")
|
||||
if (props.viewConfigs & ViewConfigs.FadeRead && props.item.hasRead)
|
||||
cn.push("read")
|
||||
return cn.join(" ")
|
||||
}
|
||||
|
||||
const ListCard: React.FunctionComponent<Card.Props> = (props) => (
|
||||
const ListCard: React.FunctionComponent<Card.Props> = props => (
|
||||
<div
|
||||
className={className(props)}
|
||||
{...Card.bindEventsToProps(props)}
|
||||
data-iid={props.item._id}
|
||||
data-is-focusable>
|
||||
{props.item.thumb && (props.viewConfigs & ViewConfigs.ShowCover) ? (
|
||||
<div className="head"><img src={props.item.thumb} /></div>
|
||||
{props.item.thumb && props.viewConfigs & ViewConfigs.ShowCover ? (
|
||||
<div className="head">
|
||||
<img src={props.item.thumb} />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="data">
|
||||
<CardInfo source={props.source} item={props.item} />
|
||||
<h3 className="title"><Highlights text={props.item.title} filter={props.filter} title /></h3>
|
||||
<h3 className="title">
|
||||
<Highlights
|
||||
text={props.item.title}
|
||||
filter={props.filter}
|
||||
title
|
||||
/>
|
||||
</h3>
|
||||
{Boolean(props.viewConfigs & ViewConfigs.ShowSnippet) && (
|
||||
<p className="snippet"><Highlights text={props.item.snippet} filter={props.filter} /></p>
|
||||
<p className="snippet">
|
||||
<Highlights
|
||||
text={props.item.snippet}
|
||||
filter={props.filter}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default ListCard
|
||||
export default ListCard
|
||||
|
|
|
@ -10,23 +10,36 @@ const className = (props: Card.Props) => {
|
|||
return cn.join(" ")
|
||||
}
|
||||
|
||||
const MagazineCard: React.FunctionComponent<Card.Props> = (props) => (
|
||||
const MagazineCard: React.FunctionComponent<Card.Props> = props => (
|
||||
<div
|
||||
className={className(props)}
|
||||
{...Card.bindEventsToProps(props)}
|
||||
data-iid={props.item._id}
|
||||
data-is-focusable>
|
||||
{props.item.thumb ? (
|
||||
<div className="head"><img src={props.item.thumb} /></div>
|
||||
<div className="head">
|
||||
<img src={props.item.thumb} />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="data">
|
||||
<div>
|
||||
<h3 className="title"><Highlights text={props.item.title} filter={props.filter} title /></h3>
|
||||
<p className="snippet"><Highlights text={props.item.snippet} filter={props.filter} /></p>
|
||||
<h3 className="title">
|
||||
<Highlights
|
||||
text={props.item.title}
|
||||
filter={props.filter}
|
||||
title
|
||||
/>
|
||||
</h3>
|
||||
<p className="snippet">
|
||||
<Highlights
|
||||
text={props.item.snippet}
|
||||
filter={props.filter}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<CardInfo source={props.source} item={props.item} showCreator />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default MagazineCard
|
||||
export default MagazineCard
|
||||
|
|
|
@ -1,8 +1,18 @@
|
|||
import * as React from "react"
|
||||
import intl from "react-intl-universal"
|
||||
import QRCode from "qrcode.react"
|
||||
import { cutText, webSearch, getSearchEngineName, platformCtrl } from "../scripts/utils"
|
||||
import { ContextualMenu, IContextualMenuItem, ContextualMenuItemType, DirectionalHint } from "office-ui-fabric-react/lib/ContextualMenu"
|
||||
import {
|
||||
cutText,
|
||||
webSearch,
|
||||
getSearchEngineName,
|
||||
platformCtrl,
|
||||
} from "../scripts/utils"
|
||||
import {
|
||||
ContextualMenu,
|
||||
IContextualMenuItem,
|
||||
ContextualMenuItemType,
|
||||
DirectionalHint,
|
||||
} from "office-ui-fabric-react/lib/ContextualMenu"
|
||||
import { ContextMenuType } from "../scripts/models/app"
|
||||
import { RSSItem } from "../scripts/models/item"
|
||||
import { ContextReduxProps } from "../containers/context-menu-container"
|
||||
|
@ -37,15 +47,12 @@ export type ContextMenuProps = ContextReduxProps & {
|
|||
}
|
||||
|
||||
export const shareSubmenu = (item: RSSItem): IContextualMenuItem[] => [
|
||||
{ key: "qr", url: item.link, onRender: renderShareQR }
|
||||
{ key: "qr", url: item.link, onRender: renderShareQR },
|
||||
]
|
||||
|
||||
export const renderShareQR = (item: IContextualMenuItem) => (
|
||||
<div className="qr-container">
|
||||
<QRCode
|
||||
value={item.url}
|
||||
size={150}
|
||||
renderAs="svg" />
|
||||
<QRCode value={item.url} size={150} renderAs="svg" />
|
||||
</div>
|
||||
)
|
||||
|
||||
|
@ -55,143 +62,222 @@ function getSearchItem(text: string): IContextualMenuItem {
|
|||
key: "searchText",
|
||||
text: intl.get("context.search", {
|
||||
text: cutText(text, 15),
|
||||
engine: getSearchEngineName(engine)
|
||||
engine: getSearchEngineName(engine),
|
||||
}),
|
||||
iconProps: { iconName: "Search" },
|
||||
onClick: () => webSearch(text, engine)
|
||||
onClick: () => webSearch(text, engine),
|
||||
}
|
||||
}
|
||||
|
||||
export class ContextMenu extends React.Component<ContextMenuProps> {
|
||||
getItems = (): IContextualMenuItem[] => {
|
||||
switch (this.props.type) {
|
||||
case ContextMenuType.Item: return [
|
||||
{
|
||||
key: "showItem",
|
||||
text: intl.get("context.read"),
|
||||
iconProps: { iconName: "TextDocument" },
|
||||
onClick: () => {
|
||||
this.props.markRead(this.props.item)
|
||||
this.props.showItem(this.props.feedId, this.props.item)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "openInBrowser",
|
||||
text: intl.get("openExternal"),
|
||||
iconProps: { iconName: "NavigateExternalInline" },
|
||||
onClick: (e) => {
|
||||
this.props.markRead(this.props.item)
|
||||
window.utils.openExternal(this.props.item.link, platformCtrl(e))
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "markAsRead",
|
||||
text: this.props.item.hasRead ? intl.get("article.markUnread") : intl.get("article.markRead"),
|
||||
iconProps: this.props.item.hasRead
|
||||
? { iconName: "RadioBtnOn", style: { fontSize: 14, textAlign: "center" } }
|
||||
: { iconName: "StatusCircleRing" },
|
||||
onClick: () => {
|
||||
if (this.props.item.hasRead) this.props.markUnread(this.props.item)
|
||||
else this.props.markRead(this.props.item)
|
||||
},
|
||||
split: true,
|
||||
subMenuProps: {
|
||||
items: [
|
||||
{
|
||||
key: "markBelow",
|
||||
text: intl.get("article.markBelow"),
|
||||
iconProps: { iconName: "Down", style: { fontSize: 14 } },
|
||||
onClick: () => this.props.markAllRead(null, this.props.item.date)
|
||||
},
|
||||
{
|
||||
key: "markAbove",
|
||||
text: intl.get("article.markAbove"),
|
||||
iconProps: { iconName: "Up", style: { fontSize: 14 } },
|
||||
onClick: () => this.props.markAllRead(null, this.props.item.date, false)
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "toggleStarred",
|
||||
text: this.props.item.starred ? intl.get("article.unstar") : intl.get("article.star"),
|
||||
iconProps: { iconName: this.props.item.starred ? "FavoriteStar" : "FavoriteStarFill" },
|
||||
onClick: () => { this.props.toggleStarred(this.props.item) }
|
||||
},
|
||||
{
|
||||
key: "toggleHidden",
|
||||
text: this.props.item.hidden ? intl.get("article.unhide") : intl.get("article.hide"),
|
||||
iconProps: { iconName: this.props.item.hidden ? "View" : "Hide3" },
|
||||
onClick: () => { this.props.toggleHidden(this.props.item) }
|
||||
},
|
||||
{
|
||||
key: "divider_1",
|
||||
itemType: ContextualMenuItemType.Divider,
|
||||
},
|
||||
{
|
||||
key: "share",
|
||||
text: intl.get("context.share"),
|
||||
iconProps: { iconName: "Share" },
|
||||
subMenuProps: {
|
||||
items: shareSubmenu(this.props.item)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "copyTitle",
|
||||
text: intl.get("context.copyTitle"),
|
||||
onClick: () => { window.utils.writeClipboard(this.props.item.title) }
|
||||
},
|
||||
{
|
||||
key: "copyURL",
|
||||
text: intl.get("context.copyURL"),
|
||||
onClick: () => { window.utils.writeClipboard(this.props.item.link) }
|
||||
},
|
||||
...(this.props.viewConfigs !== undefined ? [
|
||||
case ContextMenuType.Item:
|
||||
return [
|
||||
{
|
||||
key: "divider_2",
|
||||
itemType: ContextualMenuItemType.Divider,
|
||||
key: "showItem",
|
||||
text: intl.get("context.read"),
|
||||
iconProps: { iconName: "TextDocument" },
|
||||
onClick: () => {
|
||||
this.props.markRead(this.props.item)
|
||||
this.props.showItem(
|
||||
this.props.feedId,
|
||||
this.props.item
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "view",
|
||||
text: intl.get("context.view"),
|
||||
key: "openInBrowser",
|
||||
text: intl.get("openExternal"),
|
||||
iconProps: { iconName: "NavigateExternalInline" },
|
||||
onClick: e => {
|
||||
this.props.markRead(this.props.item)
|
||||
window.utils.openExternal(
|
||||
this.props.item.link,
|
||||
platformCtrl(e)
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "markAsRead",
|
||||
text: this.props.item.hasRead
|
||||
? intl.get("article.markUnread")
|
||||
: intl.get("article.markRead"),
|
||||
iconProps: this.props.item.hasRead
|
||||
? {
|
||||
iconName: "RadioBtnOn",
|
||||
style: { fontSize: 14, textAlign: "center" },
|
||||
}
|
||||
: { iconName: "StatusCircleRing" },
|
||||
onClick: () => {
|
||||
if (this.props.item.hasRead)
|
||||
this.props.markUnread(this.props.item)
|
||||
else this.props.markRead(this.props.item)
|
||||
},
|
||||
split: true,
|
||||
subMenuProps: {
|
||||
items: [
|
||||
{
|
||||
key: "showCover",
|
||||
text: intl.get("context.showCover"),
|
||||
canCheck: true,
|
||||
checked: Boolean(this.props.viewConfigs & ViewConfigs.ShowCover),
|
||||
onClick: () => this.props.setViewConfigs(this.props.viewConfigs ^ ViewConfigs.ShowCover)
|
||||
key: "markBelow",
|
||||
text: intl.get("article.markBelow"),
|
||||
iconProps: {
|
||||
iconName: "Down",
|
||||
style: { fontSize: 14 },
|
||||
},
|
||||
onClick: () =>
|
||||
this.props.markAllRead(
|
||||
null,
|
||||
this.props.item.date
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "showSnippet",
|
||||
text: intl.get("context.showSnippet"),
|
||||
canCheck: true,
|
||||
checked: Boolean(this.props.viewConfigs & ViewConfigs.ShowSnippet),
|
||||
onClick: () => this.props.setViewConfigs(this.props.viewConfigs ^ ViewConfigs.ShowSnippet)
|
||||
key: "markAbove",
|
||||
text: intl.get("article.markAbove"),
|
||||
iconProps: {
|
||||
iconName: "Up",
|
||||
style: { fontSize: 14 },
|
||||
},
|
||||
onClick: () =>
|
||||
this.props.markAllRead(
|
||||
null,
|
||||
this.props.item.date,
|
||||
false
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "fadeRead",
|
||||
text: intl.get("context.fadeRead"),
|
||||
canCheck: true,
|
||||
checked: Boolean(this.props.viewConfigs & ViewConfigs.FadeRead),
|
||||
onClick: () => this.props.setViewConfigs(this.props.viewConfigs ^ ViewConfigs.FadeRead)
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
] : [])
|
||||
]
|
||||
case ContextMenuType.Text: {
|
||||
const items: IContextualMenuItem[] = this.props.text? [
|
||||
{
|
||||
key: "copyText",
|
||||
text: intl.get("context.copy"),
|
||||
iconProps: { iconName: "Copy" },
|
||||
onClick: () => { window.utils.writeClipboard(this.props.text) }
|
||||
key: "toggleStarred",
|
||||
text: this.props.item.starred
|
||||
? intl.get("article.unstar")
|
||||
: intl.get("article.star"),
|
||||
iconProps: {
|
||||
iconName: this.props.item.starred
|
||||
? "FavoriteStar"
|
||||
: "FavoriteStarFill",
|
||||
},
|
||||
onClick: () => {
|
||||
this.props.toggleStarred(this.props.item)
|
||||
},
|
||||
},
|
||||
getSearchItem(this.props.text)
|
||||
] : []
|
||||
{
|
||||
key: "toggleHidden",
|
||||
text: this.props.item.hidden
|
||||
? intl.get("article.unhide")
|
||||
: intl.get("article.hide"),
|
||||
iconProps: {
|
||||
iconName: this.props.item.hidden ? "View" : "Hide3",
|
||||
},
|
||||
onClick: () => {
|
||||
this.props.toggleHidden(this.props.item)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "divider_1",
|
||||
itemType: ContextualMenuItemType.Divider,
|
||||
},
|
||||
{
|
||||
key: "share",
|
||||
text: intl.get("context.share"),
|
||||
iconProps: { iconName: "Share" },
|
||||
subMenuProps: {
|
||||
items: shareSubmenu(this.props.item),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "copyTitle",
|
||||
text: intl.get("context.copyTitle"),
|
||||
onClick: () => {
|
||||
window.utils.writeClipboard(this.props.item.title)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "copyURL",
|
||||
text: intl.get("context.copyURL"),
|
||||
onClick: () => {
|
||||
window.utils.writeClipboard(this.props.item.link)
|
||||
},
|
||||
},
|
||||
...(this.props.viewConfigs !== undefined
|
||||
? [
|
||||
{
|
||||
key: "divider_2",
|
||||
itemType: ContextualMenuItemType.Divider,
|
||||
},
|
||||
{
|
||||
key: "view",
|
||||
text: intl.get("context.view"),
|
||||
subMenuProps: {
|
||||
items: [
|
||||
{
|
||||
key: "showCover",
|
||||
text: intl.get(
|
||||
"context.showCover"
|
||||
),
|
||||
canCheck: true,
|
||||
checked: Boolean(
|
||||
this.props.viewConfigs &
|
||||
ViewConfigs.ShowCover
|
||||
),
|
||||
onClick: () =>
|
||||
this.props.setViewConfigs(
|
||||
this.props.viewConfigs ^
|
||||
ViewConfigs.ShowCover
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "showSnippet",
|
||||
text: intl.get(
|
||||
"context.showSnippet"
|
||||
),
|
||||
canCheck: true,
|
||||
checked: Boolean(
|
||||
this.props.viewConfigs &
|
||||
ViewConfigs.ShowSnippet
|
||||
),
|
||||
onClick: () =>
|
||||
this.props.setViewConfigs(
|
||||
this.props.viewConfigs ^
|
||||
ViewConfigs.ShowSnippet
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "fadeRead",
|
||||
text: intl.get(
|
||||
"context.fadeRead"
|
||||
),
|
||||
canCheck: true,
|
||||
checked: Boolean(
|
||||
this.props.viewConfigs &
|
||||
ViewConfigs.FadeRead
|
||||
),
|
||||
onClick: () =>
|
||||
this.props.setViewConfigs(
|
||||
this.props.viewConfigs ^
|
||||
ViewConfigs.FadeRead
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
case ContextMenuType.Text: {
|
||||
const items: IContextualMenuItem[] = this.props.text
|
||||
? [
|
||||
{
|
||||
key: "copyText",
|
||||
text: intl.get("context.copy"),
|
||||
iconProps: { iconName: "Copy" },
|
||||
onClick: () => {
|
||||
window.utils.writeClipboard(this.props.text)
|
||||
},
|
||||
},
|
||||
getSearchItem(this.props.text),
|
||||
]
|
||||
: []
|
||||
if (this.props.url) {
|
||||
items.push({
|
||||
key: "urlSection",
|
||||
|
@ -202,229 +288,320 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
|
|||
{
|
||||
key: "openInBrowser",
|
||||
text: intl.get("openExternal"),
|
||||
iconProps: { iconName: "NavigateExternalInline" },
|
||||
onClick: (e) => { window.utils.openExternal(this.props.url, platformCtrl(e)) }
|
||||
iconProps: {
|
||||
iconName: "NavigateExternalInline",
|
||||
},
|
||||
onClick: e => {
|
||||
window.utils.openExternal(
|
||||
this.props.url,
|
||||
platformCtrl(e)
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "copyURL",
|
||||
text: intl.get("context.copyURL"),
|
||||
iconProps: { iconName: "Link" },
|
||||
onClick: () => { window.utils.writeClipboard(this.props.url) }
|
||||
}
|
||||
]
|
||||
}
|
||||
onClick: () => {
|
||||
window.utils.writeClipboard(
|
||||
this.props.url
|
||||
)
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
case ContextMenuType.Image: return [
|
||||
{
|
||||
key: "openInBrowser",
|
||||
text: intl.get("openExternal"),
|
||||
iconProps: { iconName: "NavigateExternalInline" },
|
||||
onClick: (e) => {
|
||||
if (platformCtrl(e)) {
|
||||
window.utils.imageCallback(ImageCallbackTypes.OpenExternalBg)
|
||||
} else {
|
||||
window.utils.imageCallback(ImageCallbackTypes.OpenExternal)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "saveImageAs",
|
||||
text: intl.get("context.saveImageAs"),
|
||||
iconProps: { iconName: "SaveTemplate" },
|
||||
onClick: () => { window.utils.imageCallback(ImageCallbackTypes.SaveAs) }
|
||||
},
|
||||
{
|
||||
key: "copyImage",
|
||||
text: intl.get("context.copyImage"),
|
||||
iconProps: { iconName: "FileImage" },
|
||||
onClick: () => { window.utils.imageCallback(ImageCallbackTypes.Copy) }
|
||||
},
|
||||
{
|
||||
key: "copyImageURL",
|
||||
text: intl.get("context.copyImageURL"),
|
||||
iconProps: { iconName: "Link" },
|
||||
onClick: () => { window.utils.imageCallback(ImageCallbackTypes.CopyLink) }
|
||||
}
|
||||
]
|
||||
case ContextMenuType.View: return [
|
||||
{
|
||||
key: "section_1",
|
||||
itemType: ContextualMenuItemType.Section,
|
||||
sectionProps: {
|
||||
title: intl.get("context.view"),
|
||||
bottomDivider: true,
|
||||
items: [
|
||||
{
|
||||
key: "cardView",
|
||||
text: intl.get("context.cardView"),
|
||||
iconProps: { iconName: "GridViewMedium" },
|
||||
canCheck: true,
|
||||
checked: this.props.viewType === ViewType.Cards,
|
||||
onClick: () => this.props.switchView(ViewType.Cards)
|
||||
},
|
||||
{
|
||||
key: "listView",
|
||||
text: intl.get("context.listView"),
|
||||
iconProps: { iconName: "BacklogList" },
|
||||
canCheck: true,
|
||||
checked: this.props.viewType === ViewType.List,
|
||||
onClick: () => this.props.switchView(ViewType.List)
|
||||
},
|
||||
{
|
||||
key: "magazineView",
|
||||
text: intl.get("context.magazineView"),
|
||||
iconProps: { iconName: "Articles" },
|
||||
canCheck: true,
|
||||
checked: this.props.viewType === ViewType.Magazine,
|
||||
onClick: () => this.props.switchView(ViewType.Magazine)
|
||||
},
|
||||
{
|
||||
key: "compactView",
|
||||
text: intl.get("context.compactView"),
|
||||
iconProps: { iconName: "BulletedList" },
|
||||
canCheck: true,
|
||||
checked: this.props.viewType === ViewType.Compact,
|
||||
onClick: () => this.props.switchView(ViewType.Compact)
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "section_2",
|
||||
itemType: ContextualMenuItemType.Section,
|
||||
sectionProps: {
|
||||
title: intl.get("context.filter"),
|
||||
bottomDivider: true,
|
||||
items: [
|
||||
{
|
||||
key: "allArticles",
|
||||
text: intl.get("allArticles"),
|
||||
iconProps: { iconName: "ClearFilter" },
|
||||
canCheck: true,
|
||||
checked: (this.props.filter & ~FilterType.Toggles) == FilterType.Default,
|
||||
onClick: () => this.props.switchFilter(FilterType.Default)
|
||||
},
|
||||
{
|
||||
key: "unreadOnly",
|
||||
text: intl.get("context.unreadOnly"),
|
||||
iconProps: { iconName: "RadioBtnOn", style: { fontSize: 14, textAlign: "center" } },
|
||||
canCheck: true,
|
||||
checked: (this.props.filter & ~FilterType.Toggles) == FilterType.UnreadOnly,
|
||||
onClick: () => this.props.switchFilter(FilterType.UnreadOnly)
|
||||
},
|
||||
{
|
||||
key: "starredOnly",
|
||||
text: intl.get("context.starredOnly"),
|
||||
iconProps: { iconName: "FavoriteStarFill" },
|
||||
canCheck: true,
|
||||
checked: (this.props.filter & ~FilterType.Toggles) == FilterType.StarredOnly,
|
||||
onClick: () => this.props.switchFilter(FilterType.StarredOnly)
|
||||
case ContextMenuType.Image:
|
||||
return [
|
||||
{
|
||||
key: "openInBrowser",
|
||||
text: intl.get("openExternal"),
|
||||
iconProps: { iconName: "NavigateExternalInline" },
|
||||
onClick: e => {
|
||||
if (platformCtrl(e)) {
|
||||
window.utils.imageCallback(
|
||||
ImageCallbackTypes.OpenExternalBg
|
||||
)
|
||||
} else {
|
||||
window.utils.imageCallback(
|
||||
ImageCallbackTypes.OpenExternal
|
||||
)
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "section_3",
|
||||
itemType: ContextualMenuItemType.Section,
|
||||
sectionProps: {
|
||||
title: intl.get("search"),
|
||||
bottomDivider: true,
|
||||
items: [
|
||||
{
|
||||
key: "caseSensitive",
|
||||
text: intl.get("context.caseSensitive"),
|
||||
iconProps: { style: { fontSize: 12, fontStyle: "normal" }, children: "Aa" },
|
||||
canCheck: true,
|
||||
checked: !(this.props.filter & FilterType.CaseInsensitive),
|
||||
onClick: () => this.props.toggleFilter(FilterType.CaseInsensitive)
|
||||
},
|
||||
{
|
||||
key: "fullSearch",
|
||||
text: intl.get("context.fullSearch"),
|
||||
iconProps: { iconName: "Breadcrumb" },
|
||||
canCheck: true,
|
||||
checked: Boolean(this.props.filter & FilterType.FullSearch),
|
||||
onClick: () => this.props.toggleFilter(FilterType.FullSearch)
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "showHidden",
|
||||
text: intl.get("context.showHidden"),
|
||||
canCheck: true,
|
||||
checked: Boolean(this.props.filter & FilterType.ShowHidden),
|
||||
onClick: () => this.props.toggleFilter(FilterType.ShowHidden)
|
||||
}
|
||||
]
|
||||
case ContextMenuType.Group: return [
|
||||
{
|
||||
key: "markAllRead",
|
||||
text: intl.get("nav.markAllRead"),
|
||||
iconProps: { iconName: "CheckMark" },
|
||||
onClick: () => this.props.markAllRead(this.props.sids)
|
||||
},
|
||||
{
|
||||
key: "refresh",
|
||||
text: intl.get("nav.refresh"),
|
||||
iconProps: { iconName: "Sync" },
|
||||
onClick: () => this.props.fetchItems(this.props.sids)
|
||||
},
|
||||
{
|
||||
key: "manage",
|
||||
text: intl.get("context.manageSources"),
|
||||
iconProps: { iconName: "Settings" },
|
||||
onClick: () => this.props.settings(this.props.sids)
|
||||
}
|
||||
]
|
||||
case ContextMenuType.MarkRead: return [
|
||||
{
|
||||
key: "section_1",
|
||||
itemType: ContextualMenuItemType.Section,
|
||||
sectionProps: {
|
||||
title: intl.get("nav.markAllRead"),
|
||||
items: [
|
||||
{
|
||||
key: "all",
|
||||
text: intl.get("allArticles"),
|
||||
iconProps: { iconName: "ReceiptCheck" },
|
||||
onClick: () => this.props.markAllRead()
|
||||
},
|
||||
{
|
||||
key: "1d",
|
||||
text: intl.get("app.daysAgo", { days: 1 }),
|
||||
onClick: () => {
|
||||
let date = new Date()
|
||||
date.setTime(date.getTime() - 86400000)
|
||||
this.props.markAllRead(null, date)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "3d",
|
||||
text: intl.get("app.daysAgo", { days: 3 }),
|
||||
onClick: () => {
|
||||
let date = new Date()
|
||||
date.setTime(date.getTime() - 3 * 86400000)
|
||||
this.props.markAllRead(null, date)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "7d",
|
||||
text: intl.get("app.daysAgo", { days: 7 }),
|
||||
onClick: () => {
|
||||
let date = new Date()
|
||||
date.setTime(date.getTime() - 7 * 86400000)
|
||||
this.props.markAllRead(null, date)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
default: return []
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "saveImageAs",
|
||||
text: intl.get("context.saveImageAs"),
|
||||
iconProps: { iconName: "SaveTemplate" },
|
||||
onClick: () => {
|
||||
window.utils.imageCallback(
|
||||
ImageCallbackTypes.SaveAs
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "copyImage",
|
||||
text: intl.get("context.copyImage"),
|
||||
iconProps: { iconName: "FileImage" },
|
||||
onClick: () => {
|
||||
window.utils.imageCallback(ImageCallbackTypes.Copy)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "copyImageURL",
|
||||
text: intl.get("context.copyImageURL"),
|
||||
iconProps: { iconName: "Link" },
|
||||
onClick: () => {
|
||||
window.utils.imageCallback(
|
||||
ImageCallbackTypes.CopyLink
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
case ContextMenuType.View:
|
||||
return [
|
||||
{
|
||||
key: "section_1",
|
||||
itemType: ContextualMenuItemType.Section,
|
||||
sectionProps: {
|
||||
title: intl.get("context.view"),
|
||||
bottomDivider: true,
|
||||
items: [
|
||||
{
|
||||
key: "cardView",
|
||||
text: intl.get("context.cardView"),
|
||||
iconProps: { iconName: "GridViewMedium" },
|
||||
canCheck: true,
|
||||
checked:
|
||||
this.props.viewType === ViewType.Cards,
|
||||
onClick: () =>
|
||||
this.props.switchView(ViewType.Cards),
|
||||
},
|
||||
{
|
||||
key: "listView",
|
||||
text: intl.get("context.listView"),
|
||||
iconProps: { iconName: "BacklogList" },
|
||||
canCheck: true,
|
||||
checked:
|
||||
this.props.viewType === ViewType.List,
|
||||
onClick: () =>
|
||||
this.props.switchView(ViewType.List),
|
||||
},
|
||||
{
|
||||
key: "magazineView",
|
||||
text: intl.get("context.magazineView"),
|
||||
iconProps: { iconName: "Articles" },
|
||||
canCheck: true,
|
||||
checked:
|
||||
this.props.viewType ===
|
||||
ViewType.Magazine,
|
||||
onClick: () =>
|
||||
this.props.switchView(
|
||||
ViewType.Magazine
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "compactView",
|
||||
text: intl.get("context.compactView"),
|
||||
iconProps: { iconName: "BulletedList" },
|
||||
canCheck: true,
|
||||
checked:
|
||||
this.props.viewType ===
|
||||
ViewType.Compact,
|
||||
onClick: () =>
|
||||
this.props.switchView(ViewType.Compact),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "section_2",
|
||||
itemType: ContextualMenuItemType.Section,
|
||||
sectionProps: {
|
||||
title: intl.get("context.filter"),
|
||||
bottomDivider: true,
|
||||
items: [
|
||||
{
|
||||
key: "allArticles",
|
||||
text: intl.get("allArticles"),
|
||||
iconProps: { iconName: "ClearFilter" },
|
||||
canCheck: true,
|
||||
checked:
|
||||
(this.props.filter &
|
||||
~FilterType.Toggles) ==
|
||||
FilterType.Default,
|
||||
onClick: () =>
|
||||
this.props.switchFilter(
|
||||
FilterType.Default
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "unreadOnly",
|
||||
text: intl.get("context.unreadOnly"),
|
||||
iconProps: {
|
||||
iconName: "RadioBtnOn",
|
||||
style: {
|
||||
fontSize: 14,
|
||||
textAlign: "center",
|
||||
},
|
||||
},
|
||||
canCheck: true,
|
||||
checked:
|
||||
(this.props.filter &
|
||||
~FilterType.Toggles) ==
|
||||
FilterType.UnreadOnly,
|
||||
onClick: () =>
|
||||
this.props.switchFilter(
|
||||
FilterType.UnreadOnly
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "starredOnly",
|
||||
text: intl.get("context.starredOnly"),
|
||||
iconProps: { iconName: "FavoriteStarFill" },
|
||||
canCheck: true,
|
||||
checked:
|
||||
(this.props.filter &
|
||||
~FilterType.Toggles) ==
|
||||
FilterType.StarredOnly,
|
||||
onClick: () =>
|
||||
this.props.switchFilter(
|
||||
FilterType.StarredOnly
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "section_3",
|
||||
itemType: ContextualMenuItemType.Section,
|
||||
sectionProps: {
|
||||
title: intl.get("search"),
|
||||
bottomDivider: true,
|
||||
items: [
|
||||
{
|
||||
key: "caseSensitive",
|
||||
text: intl.get("context.caseSensitive"),
|
||||
iconProps: {
|
||||
style: {
|
||||
fontSize: 12,
|
||||
fontStyle: "normal",
|
||||
},
|
||||
children: "Aa",
|
||||
},
|
||||
canCheck: true,
|
||||
checked: !(
|
||||
this.props.filter &
|
||||
FilterType.CaseInsensitive
|
||||
),
|
||||
onClick: () =>
|
||||
this.props.toggleFilter(
|
||||
FilterType.CaseInsensitive
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "fullSearch",
|
||||
text: intl.get("context.fullSearch"),
|
||||
iconProps: { iconName: "Breadcrumb" },
|
||||
canCheck: true,
|
||||
checked: Boolean(
|
||||
this.props.filter &
|
||||
FilterType.FullSearch
|
||||
),
|
||||
onClick: () =>
|
||||
this.props.toggleFilter(
|
||||
FilterType.FullSearch
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "showHidden",
|
||||
text: intl.get("context.showHidden"),
|
||||
canCheck: true,
|
||||
checked: Boolean(
|
||||
this.props.filter & FilterType.ShowHidden
|
||||
),
|
||||
onClick: () =>
|
||||
this.props.toggleFilter(FilterType.ShowHidden),
|
||||
},
|
||||
]
|
||||
case ContextMenuType.Group:
|
||||
return [
|
||||
{
|
||||
key: "markAllRead",
|
||||
text: intl.get("nav.markAllRead"),
|
||||
iconProps: { iconName: "CheckMark" },
|
||||
onClick: () => this.props.markAllRead(this.props.sids),
|
||||
},
|
||||
{
|
||||
key: "refresh",
|
||||
text: intl.get("nav.refresh"),
|
||||
iconProps: { iconName: "Sync" },
|
||||
onClick: () => this.props.fetchItems(this.props.sids),
|
||||
},
|
||||
{
|
||||
key: "manage",
|
||||
text: intl.get("context.manageSources"),
|
||||
iconProps: { iconName: "Settings" },
|
||||
onClick: () => this.props.settings(this.props.sids),
|
||||
},
|
||||
]
|
||||
case ContextMenuType.MarkRead:
|
||||
return [
|
||||
{
|
||||
key: "section_1",
|
||||
itemType: ContextualMenuItemType.Section,
|
||||
sectionProps: {
|
||||
title: intl.get("nav.markAllRead"),
|
||||
items: [
|
||||
{
|
||||
key: "all",
|
||||
text: intl.get("allArticles"),
|
||||
iconProps: { iconName: "ReceiptCheck" },
|
||||
onClick: () => this.props.markAllRead(),
|
||||
},
|
||||
{
|
||||
key: "1d",
|
||||
text: intl.get("app.daysAgo", { days: 1 }),
|
||||
onClick: () => {
|
||||
let date = new Date()
|
||||
date.setTime(date.getTime() - 86400000)
|
||||
this.props.markAllRead(null, date)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "3d",
|
||||
text: intl.get("app.daysAgo", { days: 3 }),
|
||||
onClick: () => {
|
||||
let date = new Date()
|
||||
date.setTime(
|
||||
date.getTime() - 3 * 86400000
|
||||
)
|
||||
this.props.markAllRead(null, date)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "7d",
|
||||
text: intl.get("app.daysAgo", { days: 7 }),
|
||||
onClick: () => {
|
||||
let date = new Date()
|
||||
date.setTime(
|
||||
date.getTime() - 7 * 86400000
|
||||
)
|
||||
this.props.markAllRead(null, date)
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -432,9 +609,16 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
|
|||
return this.props.type == ContextMenuType.Hidden ? null : (
|
||||
<ContextualMenu
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
items={this.getItems()}
|
||||
target={this.props.event || this.props.position && {left: this.props.position[0], top: this.props.position[1]}}
|
||||
onDismiss={this.props.close} />
|
||||
items={this.getItems()}
|
||||
target={
|
||||
this.props.event ||
|
||||
(this.props.position && {
|
||||
left: this.props.position[0],
|
||||
top: this.props.position[1],
|
||||
})
|
||||
}
|
||||
onDismiss={this.props.close}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,9 @@ import * as React from "react"
|
|||
import intl from "react-intl-universal"
|
||||
import { FeedProps } from "./feed"
|
||||
import DefaultCard from "../cards/default-card"
|
||||
import { PrimaryButton, FocusZone } from 'office-ui-fabric-react';
|
||||
import { RSSItem } from "../../scripts/models/item";
|
||||
import { List, AnimationClassNames } from "@fluentui/react";
|
||||
import { PrimaryButton, FocusZone } from "office-ui-fabric-react"
|
||||
import { RSSItem } from "../../scripts/models/item"
|
||||
import { List, AnimationClassNames } from "@fluentui/react"
|
||||
|
||||
class CardsFeed extends React.Component<FeedProps> {
|
||||
observer: ResizeObserver
|
||||
|
@ -12,12 +12,17 @@ class CardsFeed extends React.Component<FeedProps> {
|
|||
|
||||
updateWindowSize = (entries: ResizeObserverEntry[]) => {
|
||||
if (entries) {
|
||||
this.setState({ width: entries[0].contentRect.width - 40, height: window.innerHeight })
|
||||
this.setState({
|
||||
width: entries[0].contentRect.width - 40,
|
||||
height: window.innerHeight,
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({ width: document.querySelector(".main").clientWidth - 40 })
|
||||
this.setState({
|
||||
width: document.querySelector(".main").clientWidth - 40,
|
||||
})
|
||||
this.observer = new ResizeObserver(this.updateWindowSize)
|
||||
this.observer.observe(document.querySelector(".main"))
|
||||
}
|
||||
|
@ -31,33 +36,39 @@ class CardsFeed extends React.Component<FeedProps> {
|
|||
return elemPerRow * rows
|
||||
}
|
||||
getPageHeight = () => {
|
||||
return this.state.height + (304 - this.state.height % 304)
|
||||
return this.state.height + (304 - (this.state.height % 304))
|
||||
}
|
||||
|
||||
flexFixItems = () => {
|
||||
let elemPerRow = Math.floor(this.state.width / 280)
|
||||
let elemLastRow = this.props.items.length % elemPerRow
|
||||
let items = [ ...this.props.items ]
|
||||
for (let i = 0; i < (elemPerRow - elemLastRow); i += 1) items.push(null)
|
||||
let items = [...this.props.items]
|
||||
for (let i = 0; i < elemPerRow - elemLastRow; i += 1) items.push(null)
|
||||
return items
|
||||
}
|
||||
onRenderItem = (item: RSSItem, index: number) => item ? (
|
||||
<DefaultCard
|
||||
feedId={this.props.feed._id}
|
||||
key={item._id}
|
||||
item={item}
|
||||
source={this.props.sourceMap[item.source]}
|
||||
filter={this.props.filter}
|
||||
shortcuts={this.props.shortcuts}
|
||||
markRead={this.props.markRead}
|
||||
contextMenu={this.props.contextMenu}
|
||||
showItem={this.props.showItem} />
|
||||
) : (<div className="flex-fix" key={"f-"+index}></div>)
|
||||
onRenderItem = (item: RSSItem, index: number) =>
|
||||
item ? (
|
||||
<DefaultCard
|
||||
feedId={this.props.feed._id}
|
||||
key={item._id}
|
||||
item={item}
|
||||
source={this.props.sourceMap[item.source]}
|
||||
filter={this.props.filter}
|
||||
shortcuts={this.props.shortcuts}
|
||||
markRead={this.props.markRead}
|
||||
contextMenu={this.props.contextMenu}
|
||||
showItem={this.props.showItem}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex-fix" key={"f-" + index}></div>
|
||||
)
|
||||
|
||||
canFocusChild = (el: HTMLElement) => {
|
||||
if (el.id === "load-more") {
|
||||
const container = document.getElementById("refocus")
|
||||
const result = container.scrollTop > container.scrollHeight - 2 * container.offsetHeight
|
||||
const result =
|
||||
container.scrollTop >
|
||||
container.scrollHeight - 2 * container.offsetHeight
|
||||
if (!result) container.scrollTop += 100
|
||||
return result
|
||||
} else {
|
||||
|
@ -66,35 +77,42 @@ class CardsFeed extends React.Component<FeedProps> {
|
|||
}
|
||||
|
||||
render() {
|
||||
return this.props.feed.loaded && (
|
||||
<FocusZone as="div"
|
||||
id="refocus"
|
||||
className="cards-feed-container"
|
||||
shouldReceiveFocus={this.canFocusChild}
|
||||
data-is-scrollable>
|
||||
<List
|
||||
className={AnimationClassNames.slideUpIn10}
|
||||
items={this.flexFixItems()}
|
||||
onRenderCell={this.onRenderItem}
|
||||
getItemCountForPage={this.getItemCountForPage}
|
||||
getPageHeight={this.getPageHeight}
|
||||
ignoreScrollingState
|
||||
usePageCache />
|
||||
{
|
||||
(this.props.feed.loaded && !this.props.feed.allLoaded)
|
||||
? <div className="load-more-wrapper"><PrimaryButton
|
||||
id="load-more"
|
||||
text={intl.get("loadMore")}
|
||||
disabled={this.props.feed.loading}
|
||||
onClick={() => this.props.loadMore(this.props.feed)} /></div>
|
||||
: null
|
||||
}
|
||||
{ this.props.items.length === 0 && (
|
||||
<div className="empty">{intl.get("article.empty")}</div>
|
||||
)}
|
||||
</FocusZone>
|
||||
return (
|
||||
this.props.feed.loaded && (
|
||||
<FocusZone
|
||||
as="div"
|
||||
id="refocus"
|
||||
className="cards-feed-container"
|
||||
shouldReceiveFocus={this.canFocusChild}
|
||||
data-is-scrollable>
|
||||
<List
|
||||
className={AnimationClassNames.slideUpIn10}
|
||||
items={this.flexFixItems()}
|
||||
onRenderCell={this.onRenderItem}
|
||||
getItemCountForPage={this.getItemCountForPage}
|
||||
getPageHeight={this.getPageHeight}
|
||||
ignoreScrollingState
|
||||
usePageCache
|
||||
/>
|
||||
{this.props.feed.loaded && !this.props.feed.allLoaded ? (
|
||||
<div className="load-more-wrapper">
|
||||
<PrimaryButton
|
||||
id="load-more"
|
||||
text={intl.get("loadMore")}
|
||||
disabled={this.props.feed.loading}
|
||||
onClick={() =>
|
||||
this.props.loadMore(this.props.feed)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{this.props.items.length === 0 && (
|
||||
<div className="empty">{intl.get("article.empty")}</div>
|
||||
)}
|
||||
</FocusZone>
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default CardsFeed
|
||||
export default CardsFeed
|
||||
|
|
|
@ -21,17 +21,15 @@ export type FeedProps = FeedReduxProps & {
|
|||
showItem: (fid: string, item: RSSItem) => void
|
||||
}
|
||||
|
||||
export class Feed extends React.Component<FeedProps> {
|
||||
export class Feed extends React.Component<FeedProps> {
|
||||
render() {
|
||||
switch (this.props.viewType) {
|
||||
case (ViewType.Cards): return (
|
||||
<CardsFeed {...this.props} />
|
||||
)
|
||||
case (ViewType.Magazine):
|
||||
case (ViewType.Compact):
|
||||
case (ViewType.List): return (
|
||||
<ListFeed {...this.props} />
|
||||
)
|
||||
case ViewType.Cards:
|
||||
return <CardsFeed {...this.props} />
|
||||
case ViewType.Magazine:
|
||||
case ViewType.Compact:
|
||||
case ViewType.List:
|
||||
return <ListFeed {...this.props} />
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,27 @@
|
|||
import * as React from "react"
|
||||
import intl from "react-intl-universal"
|
||||
import { FeedProps } from "./feed"
|
||||
import { PrimaryButton, FocusZone, FocusZoneDirection, List } from 'office-ui-fabric-react';
|
||||
import { RSSItem } from "../../scripts/models/item";
|
||||
import { AnimationClassNames } from "@fluentui/react";
|
||||
import { ViewType } from "../../schema-types";
|
||||
import ListCard from "../cards/list-card";
|
||||
import MagazineCard from "../cards/magazine-card";
|
||||
import CompactCard from "../cards/compact-card";
|
||||
import { Card } from "../cards/card";
|
||||
import {
|
||||
PrimaryButton,
|
||||
FocusZone,
|
||||
FocusZoneDirection,
|
||||
List,
|
||||
} from "office-ui-fabric-react"
|
||||
import { RSSItem } from "../../scripts/models/item"
|
||||
import { AnimationClassNames } from "@fluentui/react"
|
||||
import { ViewType } from "../../schema-types"
|
||||
import ListCard from "../cards/list-card"
|
||||
import MagazineCard from "../cards/magazine-card"
|
||||
import CompactCard from "../cards/compact-card"
|
||||
import { Card } from "../cards/card"
|
||||
|
||||
class ListFeed extends React.Component<FeedProps> {
|
||||
onRenderItem = (item: RSSItem) => {
|
||||
const props = {
|
||||
feedId: this.props.feed._id,
|
||||
key: item._id,
|
||||
item: item,
|
||||
source: this.props.sourceMap[item.source],
|
||||
item: item,
|
||||
source: this.props.sourceMap[item.source],
|
||||
filter: this.props.filter,
|
||||
viewConfigs: this.props.viewConfigs,
|
||||
shortcuts: this.props.shortcuts,
|
||||
|
@ -24,29 +29,40 @@ class ListFeed extends React.Component<FeedProps> {
|
|||
contextMenu: this.props.contextMenu,
|
||||
showItem: this.props.showItem,
|
||||
} as Card.Props
|
||||
if (this.props.viewType === ViewType.List && this.props.currentItem === item._id) {
|
||||
if (
|
||||
this.props.viewType === ViewType.List &&
|
||||
this.props.currentItem === item._id
|
||||
) {
|
||||
props.selected = true
|
||||
}
|
||||
|
||||
switch (this.props.viewType) {
|
||||
case (ViewType.Magazine): return <MagazineCard {...props} />
|
||||
case (ViewType.Compact): return <CompactCard {...props} />
|
||||
default: return <ListCard {...props} />
|
||||
case ViewType.Magazine:
|
||||
return <MagazineCard {...props} />
|
||||
case ViewType.Compact:
|
||||
return <CompactCard {...props} />
|
||||
default:
|
||||
return <ListCard {...props} />
|
||||
}
|
||||
}
|
||||
|
||||
getClassName = () => {
|
||||
switch (this.props.viewType) {
|
||||
case (ViewType.Magazine): return "magazine-feed"
|
||||
case (ViewType.Compact): return "compact-feed"
|
||||
default: return "list-feed"
|
||||
case ViewType.Magazine:
|
||||
return "magazine-feed"
|
||||
case ViewType.Compact:
|
||||
return "compact-feed"
|
||||
default:
|
||||
return "list-feed"
|
||||
}
|
||||
}
|
||||
|
||||
canFocusChild = (el: HTMLElement) => {
|
||||
if (el.id === "load-more") {
|
||||
const container = document.getElementById("refocus")
|
||||
const result = container.scrollTop > container.scrollHeight - 2 * container.offsetHeight
|
||||
const result =
|
||||
container.scrollTop >
|
||||
container.scrollHeight - 2 * container.offsetHeight
|
||||
if (!result) container.scrollTop += 100
|
||||
return result
|
||||
} else {
|
||||
|
@ -55,34 +71,41 @@ class ListFeed extends React.Component<FeedProps> {
|
|||
}
|
||||
|
||||
render() {
|
||||
return this.props.feed.loaded && (
|
||||
<FocusZone as="div"
|
||||
id="refocus"
|
||||
direction={FocusZoneDirection.vertical}
|
||||
className={this.getClassName()}
|
||||
shouldReceiveFocus={this.canFocusChild}
|
||||
data-is-scrollable>
|
||||
<List
|
||||
className={AnimationClassNames.slideUpIn10}
|
||||
items={this.props.items}
|
||||
onRenderCell={this.onRenderItem}
|
||||
ignoreScrollingState
|
||||
usePageCache />
|
||||
{
|
||||
(this.props.feed.loaded && !this.props.feed.allLoaded)
|
||||
? <div className="load-more-wrapper"><PrimaryButton
|
||||
id="load-more"
|
||||
text={intl.get("loadMore")}
|
||||
disabled={this.props.feed.loading}
|
||||
onClick={() => this.props.loadMore(this.props.feed)} /></div>
|
||||
: null
|
||||
}
|
||||
{ this.props.items.length === 0 && (
|
||||
<div className="empty">{intl.get("article.empty")}</div>
|
||||
)}
|
||||
</FocusZone>
|
||||
return (
|
||||
this.props.feed.loaded && (
|
||||
<FocusZone
|
||||
as="div"
|
||||
id="refocus"
|
||||
direction={FocusZoneDirection.vertical}
|
||||
className={this.getClassName()}
|
||||
shouldReceiveFocus={this.canFocusChild}
|
||||
data-is-scrollable>
|
||||
<List
|
||||
className={AnimationClassNames.slideUpIn10}
|
||||
items={this.props.items}
|
||||
onRenderCell={this.onRenderItem}
|
||||
ignoreScrollingState
|
||||
usePageCache
|
||||
/>
|
||||
{this.props.feed.loaded && !this.props.feed.allLoaded ? (
|
||||
<div className="load-more-wrapper">
|
||||
<PrimaryButton
|
||||
id="load-more"
|
||||
text={intl.get("loadMore")}
|
||||
disabled={this.props.feed.loading}
|
||||
onClick={() =>
|
||||
this.props.loadMore(this.props.feed)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{this.props.items.length === 0 && (
|
||||
<div className="empty">{intl.get("article.empty")}</div>
|
||||
)}
|
||||
</FocusZone>
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default ListFeed
|
||||
export default ListFeed
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
import * as React from "react"
|
||||
import intl from "react-intl-universal"
|
||||
import { Callout, ActivityItem, Icon, DirectionalHint, Link } from "@fluentui/react"
|
||||
import {
|
||||
Callout,
|
||||
ActivityItem,
|
||||
Icon,
|
||||
DirectionalHint,
|
||||
Link,
|
||||
} from "@fluentui/react"
|
||||
import { AppLog, AppLogType } from "../scripts/models/app"
|
||||
import Time from "./utils/time"
|
||||
|
||||
|
@ -13,46 +19,67 @@ type LogMenuProps = {
|
|||
|
||||
function getLogIcon(log: AppLog) {
|
||||
switch (log.type) {
|
||||
case AppLogType.Info: return "Info"
|
||||
case AppLogType.Article: return "KnowledgeArticle"
|
||||
default: return "Warning"
|
||||
case AppLogType.Info:
|
||||
return "Info"
|
||||
case AppLogType.Article:
|
||||
return "KnowledgeArticle"
|
||||
default:
|
||||
return "Warning"
|
||||
}
|
||||
}
|
||||
|
||||
class LogMenu extends React.Component<LogMenuProps> {
|
||||
activityItems = () => this.props.logs.map((l, i) => ({
|
||||
key: i,
|
||||
activityDescription: l.iid
|
||||
? <b><Link onClick={() => this.handleArticleClick(l)}>{l.title}</Link></b>
|
||||
: <b>{l.title}</b>,
|
||||
comments: l.details,
|
||||
activityIcon: <Icon iconName={getLogIcon(l)} />,
|
||||
timeStamp: <Time date={l.time} />,
|
||||
})).reverse()
|
||||
activityItems = () =>
|
||||
this.props.logs
|
||||
.map((l, i) => ({
|
||||
key: i,
|
||||
activityDescription: l.iid ? (
|
||||
<b>
|
||||
<Link onClick={() => this.handleArticleClick(l)}>
|
||||
{l.title}
|
||||
</Link>
|
||||
</b>
|
||||
) : (
|
||||
<b>{l.title}</b>
|
||||
),
|
||||
comments: l.details,
|
||||
activityIcon: <Icon iconName={getLogIcon(l)} />,
|
||||
timeStamp: <Time date={l.time} />,
|
||||
}))
|
||||
.reverse()
|
||||
|
||||
handleArticleClick = (log: AppLog) => {
|
||||
this.props.close()
|
||||
this.props.showItem(log.iid)
|
||||
}
|
||||
|
||||
render () {
|
||||
return this.props.display && (
|
||||
<Callout
|
||||
target="#log-toggle"
|
||||
role="log-menu"
|
||||
directionalHint={DirectionalHint.bottomCenter}
|
||||
calloutWidth={320}
|
||||
calloutMaxHeight={240}
|
||||
onDismiss={this.props.close}
|
||||
>
|
||||
{ this.props.logs.length == 0
|
||||
? <p style={{ textAlign: "center" }}>{intl.get("log.empty")}</p>
|
||||
: this.activityItems().map((item => (
|
||||
<ActivityItem {...item} key={item.key} style={{ margin: 12 }} />
|
||||
))) }
|
||||
</Callout>
|
||||
render() {
|
||||
return (
|
||||
this.props.display && (
|
||||
<Callout
|
||||
target="#log-toggle"
|
||||
role="log-menu"
|
||||
directionalHint={DirectionalHint.bottomCenter}
|
||||
calloutWidth={320}
|
||||
calloutMaxHeight={240}
|
||||
onDismiss={this.props.close}>
|
||||
{this.props.logs.length == 0 ? (
|
||||
<p style={{ textAlign: "center" }}>
|
||||
{intl.get("log.empty")}
|
||||
</p>
|
||||
) : (
|
||||
this.activityItems().map(item => (
|
||||
<ActivityItem
|
||||
{...item}
|
||||
key={item.key}
|
||||
style={{ margin: 12 }}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</Callout>
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default LogMenu
|
||||
export default LogMenu
|
||||
|
|
|
@ -8,66 +8,86 @@ import { ALL } from "../scripts/models/feed"
|
|||
import { AnimationClassNames, Stack, FocusZone } from "@fluentui/react"
|
||||
|
||||
export type MenuProps = {
|
||||
status: boolean,
|
||||
display: boolean,
|
||||
selected: string,
|
||||
sources: SourceState,
|
||||
groups: SourceGroup[],
|
||||
searchOn: boolean,
|
||||
itemOn: boolean,
|
||||
toggleMenu: () => void,
|
||||
allArticles: (init?: boolean) => void,
|
||||
selectSourceGroup: (group: SourceGroup, menuKey: string) => void,
|
||||
selectSource: (source: RSSSource) => void,
|
||||
groupContextMenu: (sids: number[], event: React.MouseEvent) => void,
|
||||
updateGroupExpansion: (event: React.MouseEvent<HTMLElement>, key: string, selected: string) => void,
|
||||
toggleSearch: () => void,
|
||||
status: boolean
|
||||
display: boolean
|
||||
selected: string
|
||||
sources: SourceState
|
||||
groups: SourceGroup[]
|
||||
searchOn: boolean
|
||||
itemOn: boolean
|
||||
toggleMenu: () => void
|
||||
allArticles: (init?: boolean) => void
|
||||
selectSourceGroup: (group: SourceGroup, menuKey: string) => void
|
||||
selectSource: (source: RSSSource) => void
|
||||
groupContextMenu: (sids: number[], event: React.MouseEvent) => void
|
||||
updateGroupExpansion: (
|
||||
event: React.MouseEvent<HTMLElement>,
|
||||
key: string,
|
||||
selected: string
|
||||
) => void
|
||||
toggleSearch: () => void
|
||||
}
|
||||
|
||||
export class Menu extends React.Component<MenuProps> {
|
||||
countOverflow = (count: number) => count >= 1000 ? " 999+" : ` ${count}`
|
||||
countOverflow = (count: number) => (count >= 1000 ? " 999+" : ` ${count}`)
|
||||
|
||||
getLinkGroups = (): INavLinkGroup[] => [
|
||||
{
|
||||
links: [
|
||||
{
|
||||
name: intl.get("search"),
|
||||
ariaLabel: intl.get("search") + (this.props.searchOn ? " ✓" : " "),
|
||||
ariaLabel:
|
||||
intl.get("search") + (this.props.searchOn ? " ✓" : " "),
|
||||
key: "search",
|
||||
icon: "Search",
|
||||
onClick: this.props.toggleSearch,
|
||||
url: null
|
||||
url: null,
|
||||
},
|
||||
{
|
||||
name: intl.get("allArticles"),
|
||||
ariaLabel: intl.get("allArticles")
|
||||
+ this.countOverflow(Object.values(this.props.sources).map(s => s.unreadCount).reduce((a, b) => a + b, 0)),
|
||||
ariaLabel:
|
||||
intl.get("allArticles") +
|
||||
this.countOverflow(
|
||||
Object.values(this.props.sources)
|
||||
.map(s => s.unreadCount)
|
||||
.reduce((a, b) => a + b, 0)
|
||||
),
|
||||
key: ALL,
|
||||
icon: "TextDocument",
|
||||
onClick: () => this.props.allArticles(this.props.selected !== ALL),
|
||||
url: null
|
||||
}
|
||||
]
|
||||
onClick: () =>
|
||||
this.props.allArticles(this.props.selected !== ALL),
|
||||
url: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: intl.get("menu.subscriptions"),
|
||||
links: this.props.groups.filter(g => g.sids.length > 0).map(g => {
|
||||
if (g.isMultiple) {
|
||||
let sources = g.sids.map(sid => this.props.sources[sid])
|
||||
return {
|
||||
name: g.name,
|
||||
ariaLabel: g.name + this.countOverflow(sources.map(s => s.unreadCount).reduce((a, b) => a + b, 0)),
|
||||
key: "g-" + g.index,
|
||||
url: null,
|
||||
isExpanded: g.expanded,
|
||||
onClick: () => this.props.selectSourceGroup(g, "g-" + g.index),
|
||||
links: sources.map(this.getSource)
|
||||
links: this.props.groups
|
||||
.filter(g => g.sids.length > 0)
|
||||
.map(g => {
|
||||
if (g.isMultiple) {
|
||||
let sources = g.sids.map(sid => this.props.sources[sid])
|
||||
return {
|
||||
name: g.name,
|
||||
ariaLabel:
|
||||
g.name +
|
||||
this.countOverflow(
|
||||
sources
|
||||
.map(s => s.unreadCount)
|
||||
.reduce((a, b) => a + b, 0)
|
||||
),
|
||||
key: "g-" + g.index,
|
||||
url: null,
|
||||
isExpanded: g.expanded,
|
||||
onClick: () =>
|
||||
this.props.selectSourceGroup(g, "g-" + g.index),
|
||||
links: sources.map(this.getSource),
|
||||
}
|
||||
} else {
|
||||
return this.getSource(this.props.sources[g.sids[0]])
|
||||
}
|
||||
} else {
|
||||
return this.getSource(this.props.sources[g.sids[0]])
|
||||
}
|
||||
})
|
||||
}
|
||||
}),
|
||||
},
|
||||
]
|
||||
|
||||
getSource = (s: RSSSource): INavLink => ({
|
||||
|
@ -76,16 +96,15 @@ export class Menu extends React.Component<MenuProps> {
|
|||
key: "s-" + s.sid,
|
||||
onClick: () => this.props.selectSource(s),
|
||||
iconProps: s.iconurl ? this.getIconStyle(s.iconurl) : null,
|
||||
url: null
|
||||
url: null,
|
||||
})
|
||||
|
||||
|
||||
getIconStyle = (url: string) => ({
|
||||
style: { width: 16 },
|
||||
imageProps: {
|
||||
style: { width:"100%" },
|
||||
src: url
|
||||
}
|
||||
style: { width: "100%" },
|
||||
src: url,
|
||||
},
|
||||
})
|
||||
|
||||
onContext = (item: INavLink, event: React.MouseEvent) => {
|
||||
|
@ -104,37 +123,81 @@ export class Menu extends React.Component<MenuProps> {
|
|||
_onRenderLink = (link: INavLink): JSX.Element => {
|
||||
let count = link.ariaLabel.split(" ").pop()
|
||||
return (
|
||||
<Stack className="link-stack" horizontal grow onContextMenu={event => this.onContext(link, event)}>
|
||||
<Stack
|
||||
className="link-stack"
|
||||
horizontal
|
||||
grow
|
||||
onContextMenu={event => this.onContext(link, event)}>
|
||||
<div className="link-text">{link.name}</div>
|
||||
{count && count !== "0" && <div className="unread-count">{count}</div>}
|
||||
{count && count !== "0" && (
|
||||
<div className="unread-count">{count}</div>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
_onRenderGroupHeader = (group: INavLinkGroup): JSX.Element => {
|
||||
return <p className={"subs-header " + AnimationClassNames.slideDownIn10}>{group.name}</p>;
|
||||
return (
|
||||
<p className={"subs-header " + AnimationClassNames.slideDownIn10}>
|
||||
{group.name}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.props.status && (
|
||||
<div className={"menu-container" + (this.props.display ? " show" : "")} onClick={this.props.toggleMenu}>
|
||||
<div className={"menu" + (this.props.itemOn ? " item-on" : "")} onClick={(e) => e.stopPropagation()}>
|
||||
<div className="btn-group">
|
||||
<a className="btn hide-wide" title={intl.get("menu.close")} onClick={this.props.toggleMenu}><Icon iconName="Back" /></a>
|
||||
<a className="btn inline-block-wide" title={intl.get("menu.close")} onClick={this.props.toggleMenu}>
|
||||
<Icon iconName={window.utils.platform === "darwin" ? "SidePanel" : "GlobalNavButton"} />
|
||||
</a>
|
||||
return (
|
||||
this.props.status && (
|
||||
<div
|
||||
className={
|
||||
"menu-container" + (this.props.display ? " show" : "")
|
||||
}
|
||||
onClick={this.props.toggleMenu}>
|
||||
<div
|
||||
className={
|
||||
"menu" + (this.props.itemOn ? " item-on" : "")
|
||||
}
|
||||
onClick={e => e.stopPropagation()}>
|
||||
<div className="btn-group">
|
||||
<a
|
||||
className="btn hide-wide"
|
||||
title={intl.get("menu.close")}
|
||||
onClick={this.props.toggleMenu}>
|
||||
<Icon iconName="Back" />
|
||||
</a>
|
||||
<a
|
||||
className="btn inline-block-wide"
|
||||
title={intl.get("menu.close")}
|
||||
onClick={this.props.toggleMenu}>
|
||||
<Icon
|
||||
iconName={
|
||||
window.utils.platform === "darwin"
|
||||
? "SidePanel"
|
||||
: "GlobalNavButton"
|
||||
}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<FocusZone
|
||||
as="div"
|
||||
disabled={!this.props.display}
|
||||
className="nav-wrapper">
|
||||
<Nav
|
||||
onRenderGroupHeader={this._onRenderGroupHeader}
|
||||
onRenderLink={this._onRenderLink}
|
||||
groups={this.getLinkGroups()}
|
||||
selectedKey={this.props.selected}
|
||||
onLinkExpandClick={(event, item) =>
|
||||
this.props.updateGroupExpansion(
|
||||
event,
|
||||
item.key,
|
||||
this.props.selected
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FocusZone>
|
||||
</div>
|
||||
<FocusZone as="div" disabled={!this.props.display} className="nav-wrapper">
|
||||
<Nav
|
||||
onRenderGroupHeader={this._onRenderGroupHeader}
|
||||
onRenderLink={this._onRenderLink}
|
||||
groups={this.getLinkGroups()}
|
||||
selectedKey={this.props.selected}
|
||||
onLinkExpandClick={(event, item) => this.props.updateGroupExpansion(event, item.key, this.props.selected)} />
|
||||
</FocusZone>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ type NavProps = {
|
|||
}
|
||||
|
||||
type NavState = {
|
||||
maximized: boolean,
|
||||
maximized: boolean
|
||||
}
|
||||
|
||||
class Nav extends React.Component<NavProps, NavState> {
|
||||
|
@ -29,7 +29,7 @@ class Nav extends React.Component<NavProps, NavState> {
|
|||
this.setBodyFullscreenState(window.utils.isFullscreen())
|
||||
window.utils.addWindowStateListener(this.windowStateListener)
|
||||
this.state = {
|
||||
maximized: window.utils.isMaximized()
|
||||
maximized: window.utils.isMaximized(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,7 +87,8 @@ class Nav extends React.Component<NavProps, NavState> {
|
|||
|
||||
componentDidMount() {
|
||||
document.addEventListener("keydown", this.navShortcutsHandler)
|
||||
if (window.utils.platform === "darwin") window.utils.addTouchBarEventsListener(this.navShortcutsHandler)
|
||||
if (window.utils.platform === "darwin")
|
||||
window.utils.addTouchBarEventsListener(this.navShortcutsHandler)
|
||||
}
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener("keydown", this.navShortcutsHandler)
|
||||
|
@ -104,9 +105,12 @@ class Nav extends React.Component<NavProps, NavState> {
|
|||
window.utils.closeWindow()
|
||||
}
|
||||
|
||||
canFetch = () => this.props.state.sourceInit && this.props.state.feedInit
|
||||
&& !this.props.state.syncing && !this.props.state.fetchingItems
|
||||
fetching = () => !this.canFetch() ? " fetching" : ""
|
||||
canFetch = () =>
|
||||
this.props.state.sourceInit &&
|
||||
this.props.state.feedInit &&
|
||||
!this.props.state.syncing &&
|
||||
!this.props.state.fetchingItems
|
||||
fetching = () => (!this.canFetch() ? " fetching" : "")
|
||||
getClassNames = () => {
|
||||
const classNames = new Array<string>()
|
||||
if (this.props.state.settings.display) classNames.push("hide-btns")
|
||||
|
@ -126,7 +130,7 @@ class Nav extends React.Component<NavProps, NavState> {
|
|||
}
|
||||
|
||||
getProgress = () => {
|
||||
return this.props.state.fetchingTotal > 0
|
||||
return this.props.state.fetchingTotal > 0
|
||||
? this.props.state.fetchingProgress / this.props.state.fetchingTotal
|
||||
: null
|
||||
}
|
||||
|
@ -135,73 +139,112 @@ class Nav extends React.Component<NavProps, NavState> {
|
|||
return (
|
||||
<nav className={this.getClassNames()}>
|
||||
<div className="btn-group">
|
||||
<a className="btn hide-wide"
|
||||
title={intl.get("nav.menu")}
|
||||
<a
|
||||
className="btn hide-wide"
|
||||
title={intl.get("nav.menu")}
|
||||
onClick={this.props.menu}>
|
||||
<Icon iconName={window.utils.platform === "darwin" ? "SidePanel" : "GlobalNavButton"} />
|
||||
<Icon
|
||||
iconName={
|
||||
window.utils.platform === "darwin"
|
||||
? "SidePanel"
|
||||
: "GlobalNavButton"
|
||||
}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<span className="title">{this.props.state.title}</span>
|
||||
<div className="btn-group" style={{float:"right"}}>
|
||||
<a className={"btn"+this.fetching()}
|
||||
onClick={this.fetch}
|
||||
<div className="btn-group" style={{ float: "right" }}>
|
||||
<a
|
||||
className={"btn" + this.fetching()}
|
||||
onClick={this.fetch}
|
||||
title={intl.get("nav.refresh")}>
|
||||
<Icon iconName="Refresh" />
|
||||
</a>
|
||||
<a className="btn"
|
||||
<a
|
||||
className="btn"
|
||||
id="mark-all-toggle"
|
||||
onClick={this.props.markAllRead}
|
||||
title={intl.get("nav.markAllRead")}
|
||||
onMouseDown={e => {
|
||||
if (this.props.state.contextMenu.event === "#mark-all-toggle") e.stopPropagation()}}>
|
||||
if (
|
||||
this.props.state.contextMenu.event ===
|
||||
"#mark-all-toggle"
|
||||
)
|
||||
e.stopPropagation()
|
||||
}}>
|
||||
<Icon iconName="InboxCheck" />
|
||||
</a>
|
||||
<a className="btn"
|
||||
id="log-toggle"
|
||||
title={intl.get("nav.notifications")}
|
||||
<a
|
||||
className="btn"
|
||||
id="log-toggle"
|
||||
title={intl.get("nav.notifications")}
|
||||
onClick={this.props.logs}>
|
||||
{this.props.state.logMenu.notify ? <Icon iconName="RingerSolid" /> : <Icon iconName="Ringer" />}
|
||||
{this.props.state.logMenu.notify ? (
|
||||
<Icon iconName="RingerSolid" />
|
||||
) : (
|
||||
<Icon iconName="Ringer" />
|
||||
)}
|
||||
</a>
|
||||
<a className="btn"
|
||||
id="view-toggle"
|
||||
<a
|
||||
className="btn"
|
||||
id="view-toggle"
|
||||
title={intl.get("nav.view")}
|
||||
onClick={this.props.views}
|
||||
onClick={this.props.views}
|
||||
onMouseDown={e => {
|
||||
if (this.props.state.contextMenu.event === "#view-toggle") e.stopPropagation()}}>
|
||||
<Icon iconName="View" /></a>
|
||||
<a className="btn"
|
||||
if (
|
||||
this.props.state.contextMenu.event ===
|
||||
"#view-toggle"
|
||||
)
|
||||
e.stopPropagation()
|
||||
}}>
|
||||
<Icon iconName="View" />
|
||||
</a>
|
||||
<a
|
||||
className="btn"
|
||||
title={intl.get("nav.settings")}
|
||||
onClick={this.props.settings}>
|
||||
<Icon iconName="Settings" />
|
||||
</a>
|
||||
<span className="seperator"></span>
|
||||
<a className="btn system"
|
||||
title={intl.get("nav.minimize")}
|
||||
onClick={this.minimize}
|
||||
style={{fontSize: 12}}>
|
||||
<a
|
||||
className="btn system"
|
||||
title={intl.get("nav.minimize")}
|
||||
onClick={this.minimize}
|
||||
style={{ fontSize: 12 }}>
|
||||
<Icon iconName="Remove" />
|
||||
</a>
|
||||
<a className="btn system"
|
||||
title={intl.get("nav.maximize")}
|
||||
<a
|
||||
className="btn system"
|
||||
title={intl.get("nav.maximize")}
|
||||
onClick={this.maximize}>
|
||||
{this.state.maximized
|
||||
? <Icon iconName="ChromeRestore" style={{fontSize: 11}} />
|
||||
: <Icon iconName="Checkbox" style={{fontSize: 10}} />}
|
||||
{this.state.maximized ? (
|
||||
<Icon
|
||||
iconName="ChromeRestore"
|
||||
style={{ fontSize: 11 }}
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
iconName="Checkbox"
|
||||
style={{ fontSize: 10 }}
|
||||
/>
|
||||
)}
|
||||
</a>
|
||||
<a className="btn system close"
|
||||
<a
|
||||
className="btn system close"
|
||||
title={intl.get("close")}
|
||||
onClick={this.close}>
|
||||
<Icon iconName="Cancel" />
|
||||
</a>
|
||||
</div>
|
||||
{!this.canFetch() &&
|
||||
{!this.canFetch() && (
|
||||
<ProgressIndicator
|
||||
className="progress"
|
||||
percentComplete={this.getProgress()} />
|
||||
}
|
||||
percentComplete={this.getProgress()}
|
||||
/>
|
||||
)}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Nav
|
||||
export default Nav
|
||||
|
|
|
@ -25,59 +25,92 @@ class Page extends React.Component<PageProps> {
|
|||
prevItem = (event: React.MouseEvent) => this.offsetItem(event, -1)
|
||||
nextItem = (event: React.MouseEvent) => this.offsetItem(event, 1)
|
||||
|
||||
render = () => this.props.viewType !== ViewType.List
|
||||
? (
|
||||
<>
|
||||
{this.props.settingsOn ? null :
|
||||
<div key="card" className={"main" + (this.props.menuOn ? " menu-on" : "")}>
|
||||
<ArticleSearch />
|
||||
{this.props.feeds.map(fid => (
|
||||
<FeedContainer viewType={this.props.viewType} feedId={fid} key={fid + this.props.viewType} />
|
||||
))}
|
||||
</div>}
|
||||
{this.props.itemId && (
|
||||
<FocusTrapZone
|
||||
disabled={this.props.contextOn}
|
||||
ignoreExternalFocusing={true}
|
||||
isClickableOutsideFocusTrap={true}
|
||||
className="article-container"
|
||||
onClick={this.props.dismissItem}>
|
||||
<div className="article-wrapper" onClick={e => e.stopPropagation()}>
|
||||
<ArticleContainer itemId={this.props.itemId} />
|
||||
</div>
|
||||
{this.props.itemFromFeed && <>
|
||||
<div className="btn-group prev"><a className="btn" onClick={this.prevItem}><Icon iconName="Back" /></a></div>
|
||||
<div className="btn-group next"><a className="btn" onClick={this.nextItem}><Icon iconName="Forward" /></a></div>
|
||||
</>}
|
||||
</FocusTrapZone>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
{this.props.settingsOn ? null :
|
||||
<div key="list" className={"list-main" + (this.props.menuOn ? " menu-on" : "")}>
|
||||
<ArticleSearch />
|
||||
<div className="list-feed-container">
|
||||
{this.props.feeds.map(fid => (
|
||||
<FeedContainer viewType={this.props.viewType} feedId={fid} key={fid} />
|
||||
))}
|
||||
</div>
|
||||
{this.props.itemId
|
||||
? (
|
||||
<div className="side-article-wrapper">
|
||||
<ArticleContainer itemId={this.props.itemId} />
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className="side-logo-wrapper">
|
||||
<img className="light" src="icons/logo-outline.svg" />
|
||||
<img className="dark" src="icons/logo-outline-dark.svg" />
|
||||
render = () =>
|
||||
this.props.viewType !== ViewType.List ? (
|
||||
<>
|
||||
{this.props.settingsOn ? null : (
|
||||
<div
|
||||
key="card"
|
||||
className={
|
||||
"main" + (this.props.menuOn ? " menu-on" : "")
|
||||
}>
|
||||
<ArticleSearch />
|
||||
{this.props.feeds.map(fid => (
|
||||
<FeedContainer
|
||||
viewType={this.props.viewType}
|
||||
feedId={fid}
|
||||
key={fid + this.props.viewType}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>}
|
||||
</>
|
||||
)
|
||||
{this.props.itemId && (
|
||||
<FocusTrapZone
|
||||
disabled={this.props.contextOn}
|
||||
ignoreExternalFocusing={true}
|
||||
isClickableOutsideFocusTrap={true}
|
||||
className="article-container"
|
||||
onClick={this.props.dismissItem}>
|
||||
<div
|
||||
className="article-wrapper"
|
||||
onClick={e => e.stopPropagation()}>
|
||||
<ArticleContainer itemId={this.props.itemId} />
|
||||
</div>
|
||||
{this.props.itemFromFeed && (
|
||||
<>
|
||||
<div className="btn-group prev">
|
||||
<a className="btn" onClick={this.prevItem}>
|
||||
<Icon iconName="Back" />
|
||||
</a>
|
||||
</div>
|
||||
<div className="btn-group next">
|
||||
<a className="btn" onClick={this.nextItem}>
|
||||
<Icon iconName="Forward" />
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</FocusTrapZone>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{this.props.settingsOn ? null : (
|
||||
<div
|
||||
key="list"
|
||||
className={
|
||||
"list-main" + (this.props.menuOn ? " menu-on" : "")
|
||||
}>
|
||||
<ArticleSearch />
|
||||
<div className="list-feed-container">
|
||||
{this.props.feeds.map(fid => (
|
||||
<FeedContainer
|
||||
viewType={this.props.viewType}
|
||||
feedId={fid}
|
||||
key={fid}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{this.props.itemId ? (
|
||||
<div className="side-article-wrapper">
|
||||
<ArticleContainer itemId={this.props.itemId} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="side-logo-wrapper">
|
||||
<img
|
||||
className="light"
|
||||
src="icons/logo-outline.svg"
|
||||
/>
|
||||
<img
|
||||
className="dark"
|
||||
src="icons/logo-outline-dark.svg"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page
|
||||
export default Page
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as React from "react"
|
||||
import { connect } from 'react-redux'
|
||||
import { connect } from "react-redux"
|
||||
import { ContextMenuContainer } from "../containers/context-menu-container"
|
||||
import { closeContextMenu } from "../scripts/models/app"
|
||||
import PageContainer from "../containers/page-container"
|
||||
|
@ -9,18 +9,20 @@ import LogMenuContainer from "../containers/log-menu-container"
|
|||
import SettingsContainer from "../containers/settings-container"
|
||||
import { RootState } from "../scripts/reducer"
|
||||
|
||||
const Root = ({ locale, dispatch }) => locale && (
|
||||
<div id="root"
|
||||
key={locale}
|
||||
onMouseDown={() => dispatch(closeContextMenu())}>
|
||||
<NavContainer />
|
||||
<PageContainer />
|
||||
<LogMenuContainer />
|
||||
<MenuContainer />
|
||||
<SettingsContainer />
|
||||
<ContextMenuContainer />
|
||||
</div>
|
||||
)
|
||||
const Root = ({ locale, dispatch }) =>
|
||||
locale && (
|
||||
<div
|
||||
id="root"
|
||||
key={locale}
|
||||
onMouseDown={() => dispatch(closeContextMenu())}>
|
||||
<NavContainer />
|
||||
<PageContainer />
|
||||
<LogMenuContainer />
|
||||
<MenuContainer />
|
||||
<SettingsContainer />
|
||||
<ContextMenuContainer />
|
||||
</div>
|
||||
)
|
||||
|
||||
const getLocale = (state: RootState) => ({ locale: state.app.locale })
|
||||
export default connect(getLocale)(Root)
|
||||
export default connect(getLocale)(Root)
|
||||
|
|
|
@ -12,14 +12,14 @@ import ServiceTabContainer from "../containers/settings/service-container"
|
|||
import { initTouchBarWithTexts } from "../scripts/utils"
|
||||
|
||||
type SettingsProps = {
|
||||
display: boolean,
|
||||
blocked: boolean,
|
||||
exitting: boolean,
|
||||
display: boolean
|
||||
blocked: boolean
|
||||
exitting: boolean
|
||||
close: () => void
|
||||
}
|
||||
|
||||
class Settings extends React.Component<SettingsProps> {
|
||||
constructor(props){
|
||||
class Settings extends React.Component<SettingsProps> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,8 @@ class Settings extends React.Component<SettingsProps> {
|
|||
componentDidUpdate = (prevProps: SettingsProps) => {
|
||||
if (this.props.display !== prevProps.display) {
|
||||
if (this.props.display) {
|
||||
if (window.utils.platform === "darwin") window.utils.destroyTouchBar()
|
||||
if (window.utils.platform === "darwin")
|
||||
window.utils.destroyTouchBar()
|
||||
document.body.addEventListener("keydown", this.onKeyDown)
|
||||
} else {
|
||||
if (window.utils.platform === "darwin") initTouchBarWithTexts()
|
||||
|
@ -39,42 +40,71 @@ class Settings extends React.Component<SettingsProps> {
|
|||
}
|
||||
}
|
||||
|
||||
render = () => this.props.display && (
|
||||
<div className="settings-container">
|
||||
<div className="btn-group" style={{position: "absolute", top: 70, left: "calc(50% - 404px)"}}>
|
||||
<a className={"btn" + (this.props.exitting ? " disabled" : "")} title={intl.get("settings.exit")} onClick={this.props.close}>
|
||||
<Icon iconName="Back" />
|
||||
</a>
|
||||
render = () =>
|
||||
this.props.display && (
|
||||
<div className="settings-container">
|
||||
<div
|
||||
className="btn-group"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 70,
|
||||
left: "calc(50% - 404px)",
|
||||
}}>
|
||||
<a
|
||||
className={
|
||||
"btn" + (this.props.exitting ? " disabled" : "")
|
||||
}
|
||||
title={intl.get("settings.exit")}
|
||||
onClick={this.props.close}>
|
||||
<Icon iconName="Back" />
|
||||
</a>
|
||||
</div>
|
||||
<div className={"settings " + AnimationClassNames.slideUpIn20}>
|
||||
{this.props.blocked && (
|
||||
<FocusTrapZone
|
||||
isClickableOutsideFocusTrap={true}
|
||||
className="loading">
|
||||
<Spinner
|
||||
label={intl.get("settings.fetching")}
|
||||
tabIndex={0}
|
||||
/>
|
||||
</FocusTrapZone>
|
||||
)}
|
||||
<Pivot>
|
||||
<PivotItem
|
||||
headerText={intl.get("settings.sources")}
|
||||
itemIcon="Source">
|
||||
<SourcesTabContainer />
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerText={intl.get("settings.grouping")}
|
||||
itemIcon="GroupList">
|
||||
<GroupsTabContainer />
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerText={intl.get("settings.rules")}
|
||||
itemIcon="FilterSettings">
|
||||
<RulesTabContainer />
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerText={intl.get("settings.service")}
|
||||
itemIcon="CloudImportExport">
|
||||
<ServiceTabContainer />
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerText={intl.get("settings.app")}
|
||||
itemIcon="Settings">
|
||||
<AppTabContainer />
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerText={intl.get("settings.about")}
|
||||
itemIcon="Info">
|
||||
<AboutTab />
|
||||
</PivotItem>
|
||||
</Pivot>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"settings " + AnimationClassNames.slideUpIn20}>
|
||||
{this.props.blocked && (
|
||||
<FocusTrapZone isClickableOutsideFocusTrap={true} className="loading">
|
||||
<Spinner label={intl.get("settings.fetching")} tabIndex={0} />
|
||||
</FocusTrapZone>
|
||||
)}
|
||||
<Pivot>
|
||||
<PivotItem headerText={intl.get("settings.sources")} itemIcon="Source">
|
||||
<SourcesTabContainer />
|
||||
</PivotItem>
|
||||
<PivotItem headerText={intl.get("settings.grouping")} itemIcon="GroupList">
|
||||
<GroupsTabContainer />
|
||||
</PivotItem>
|
||||
<PivotItem headerText={intl.get("settings.rules")} itemIcon="FilterSettings">
|
||||
<RulesTabContainer />
|
||||
</PivotItem>
|
||||
<PivotItem headerText={intl.get("settings.service")} itemIcon="CloudImportExport">
|
||||
<ServiceTabContainer />
|
||||
</PivotItem>
|
||||
<PivotItem headerText={intl.get("settings.app")} itemIcon="Settings">
|
||||
<AppTabContainer />
|
||||
</PivotItem>
|
||||
<PivotItem headerText={intl.get("settings.about")} itemIcon="Info">
|
||||
<AboutTab />
|
||||
</PivotItem>
|
||||
</Pivot>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export default Settings
|
||||
export default Settings
|
||||
|
|
|
@ -6,18 +6,52 @@ class AboutTab extends React.Component {
|
|||
render = () => (
|
||||
<div className="tab-body">
|
||||
<Stack className="settings-about" horizontalAlign="center">
|
||||
<img src="icons/logo.svg" style={{width: 120, height: 120}} />
|
||||
<h3 style={{fontWeight: 600}}>Fluent Reader</h3>
|
||||
<small>{intl.get("settings.version")} {window.utils.getVersion()}</small>
|
||||
<p className="settings-hint">Copyright © 2020 Haoyuan Liu. All rights reserved.</p>
|
||||
<Stack horizontal horizontalAlign="center" tokens={{childrenGap: 12}}>
|
||||
<small><Link onClick={() => window.utils.openExternal("https://github.com/yang991178/fluent-reader/wiki/Support#keyboard-shortcuts")}>{intl.get("settings.shortcuts")}</Link></small>
|
||||
<small><Link onClick={() => window.utils.openExternal("https://github.com/yang991178/fluent-reader")}>{intl.get("settings.openSource")}</Link></small>
|
||||
<small><Link onClick={() => window.utils.openExternal("https://github.com/yang991178/fluent-reader/issues")}>{intl.get("settings.feedback")}</Link></small>
|
||||
<img src="icons/logo.svg" style={{ width: 120, height: 120 }} />
|
||||
<h3 style={{ fontWeight: 600 }}>Fluent Reader</h3>
|
||||
<small>
|
||||
{intl.get("settings.version")} {window.utils.getVersion()}
|
||||
</small>
|
||||
<p className="settings-hint">
|
||||
Copyright © 2020 Haoyuan Liu. All rights reserved.
|
||||
</p>
|
||||
<Stack
|
||||
horizontal
|
||||
horizontalAlign="center"
|
||||
tokens={{ childrenGap: 12 }}>
|
||||
<small>
|
||||
<Link
|
||||
onClick={() =>
|
||||
window.utils.openExternal(
|
||||
"https://github.com/yang991178/fluent-reader/wiki/Support#keyboard-shortcuts"
|
||||
)
|
||||
}>
|
||||
{intl.get("settings.shortcuts")}
|
||||
</Link>
|
||||
</small>
|
||||
<small>
|
||||
<Link
|
||||
onClick={() =>
|
||||
window.utils.openExternal(
|
||||
"https://github.com/yang991178/fluent-reader"
|
||||
)
|
||||
}>
|
||||
{intl.get("settings.openSource")}
|
||||
</Link>
|
||||
</small>
|
||||
<small>
|
||||
<Link
|
||||
onClick={() =>
|
||||
window.utils.openExternal(
|
||||
"https://github.com/yang991178/fluent-reader/issues"
|
||||
)
|
||||
}>
|
||||
{intl.get("settings.feedback")}
|
||||
</Link>
|
||||
</small>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AboutTab
|
||||
export default AboutTab
|
||||
|
|
|
@ -1,9 +1,30 @@
|
|||
import * as React from "react"
|
||||
import intl from "react-intl-universal"
|
||||
import { urlTest, byteToMB, calculateItemSize, getSearchEngineName } from "../../scripts/utils"
|
||||
import {
|
||||
urlTest,
|
||||
byteToMB,
|
||||
calculateItemSize,
|
||||
getSearchEngineName,
|
||||
} from "../../scripts/utils"
|
||||
import { ThemeSettings, SearchEngines } from "../../schema-types"
|
||||
import { getThemeSettings, setThemeSettings, exportAll } from "../../scripts/settings"
|
||||
import { Stack, Label, Toggle, TextField, DefaultButton, ChoiceGroup, IChoiceGroupOption, loadTheme, Dropdown, IDropdownOption, PrimaryButton } from "@fluentui/react"
|
||||
import {
|
||||
getThemeSettings,
|
||||
setThemeSettings,
|
||||
exportAll,
|
||||
} from "../../scripts/settings"
|
||||
import {
|
||||
Stack,
|
||||
Label,
|
||||
Toggle,
|
||||
TextField,
|
||||
DefaultButton,
|
||||
ChoiceGroup,
|
||||
IChoiceGroupOption,
|
||||
loadTheme,
|
||||
Dropdown,
|
||||
IDropdownOption,
|
||||
PrimaryButton,
|
||||
} from "@fluentui/react"
|
||||
import DangerButton from "../utils/danger-button"
|
||||
|
||||
type AppTabProps = {
|
||||
|
@ -31,7 +52,7 @@ class AppTab extends React.Component<AppTabProps, AppTabState> {
|
|||
themeSettings: getThemeSettings(),
|
||||
itemSize: null,
|
||||
cacheSize: null,
|
||||
deleteIndex: null
|
||||
deleteIndex: null,
|
||||
}
|
||||
this.getItemSize()
|
||||
this.getCacheSize()
|
||||
|
@ -43,7 +64,7 @@ class AppTab extends React.Component<AppTabProps, AppTabState> {
|
|||
})
|
||||
}
|
||||
getItemSize = () => {
|
||||
calculateItemSize().then((size) => {
|
||||
calculateItemSize().then(size => {
|
||||
this.setState({ itemSize: byteToMB(size) })
|
||||
})
|
||||
}
|
||||
|
@ -53,11 +74,11 @@ class AppTab extends React.Component<AppTabProps, AppTabState> {
|
|||
this.getCacheSize()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
themeChoices = (): IChoiceGroupOption[] => [
|
||||
{ key: ThemeSettings.Default, text: intl.get("followSystem") },
|
||||
{ key: ThemeSettings.Light, text: intl.get("app.lightTheme") },
|
||||
{ key: ThemeSettings.Dark, text: intl.get("app.darkTheme") }
|
||||
{ key: ThemeSettings.Dark, text: intl.get("app.darkTheme") },
|
||||
]
|
||||
|
||||
fetchIntervalOptions = (): IDropdownOption[] => [
|
||||
|
@ -73,12 +94,16 @@ class AppTab extends React.Component<AppTabProps, AppTabState> {
|
|||
this.props.setFetchInterval(item.key as number)
|
||||
}
|
||||
|
||||
searchEngineOptions = (): IDropdownOption[] => [
|
||||
SearchEngines.Google, SearchEngines.Bing, SearchEngines.Baidu, SearchEngines.DuckDuckGo
|
||||
].map(engine => ({
|
||||
key: engine,
|
||||
text: getSearchEngineName(engine)
|
||||
}))
|
||||
searchEngineOptions = (): IDropdownOption[] =>
|
||||
[
|
||||
SearchEngines.Google,
|
||||
SearchEngines.Bing,
|
||||
SearchEngines.Baidu,
|
||||
SearchEngines.DuckDuckGo,
|
||||
].map(engine => ({
|
||||
key: engine,
|
||||
text: getSearchEngineName(engine),
|
||||
}))
|
||||
onSearchEngineChanged = (item: IDropdownOption) => {
|
||||
window.settings.setSearchEngine(item.key as number)
|
||||
}
|
||||
|
@ -97,7 +122,8 @@ class AppTab extends React.Component<AppTabProps, AppTabState> {
|
|||
|
||||
confirmDelete = () => {
|
||||
this.setState({ itemSize: null })
|
||||
this.props.deleteArticles(parseInt(this.state.deleteIndex))
|
||||
this.props
|
||||
.deleteArticles(parseInt(this.state.deleteIndex))
|
||||
.then(() => this.getItemSize())
|
||||
}
|
||||
|
||||
|
@ -121,21 +147,22 @@ class AppTab extends React.Component<AppTabProps, AppTabState> {
|
|||
|
||||
toggleStatus = () => {
|
||||
window.settings.toggleProxyStatus()
|
||||
this.setState({
|
||||
this.setState({
|
||||
pacStatus: window.settings.getProxyStatus(),
|
||||
pacUrl: window.settings.getProxy()
|
||||
pacUrl: window.settings.getProxy(),
|
||||
})
|
||||
}
|
||||
|
||||
handleInputChange = (event) => {
|
||||
|
||||
handleInputChange = event => {
|
||||
const name: string = event.target.name
|
||||
// @ts-ignore
|
||||
this.setState({[name]: event.target.value.trim()})
|
||||
this.setState({ [name]: event.target.value.trim() })
|
||||
}
|
||||
|
||||
setUrl = (event: React.FormEvent) => {
|
||||
event.preventDefault()
|
||||
if (urlTest(this.state.pacUrl)) window.settings.setProxy(this.state.pacUrl)
|
||||
if (urlTest(this.state.pacUrl))
|
||||
window.settings.setProxy(this.state.pacUrl)
|
||||
}
|
||||
|
||||
onThemeChange = (_, option: IChoiceGroupOption) => {
|
||||
|
@ -148,11 +175,14 @@ class AppTab extends React.Component<AppTabProps, AppTabState> {
|
|||
<Label>{intl.get("app.language")}</Label>
|
||||
<Stack horizontal>
|
||||
<Stack.Item>
|
||||
<Dropdown
|
||||
<Dropdown
|
||||
defaultSelectedKey={window.settings.getLocaleSettings()}
|
||||
options={this.languageOptions()}
|
||||
onChanged={option => this.props.setLanguage(String(option.key))}
|
||||
style={{width: 200}} />
|
||||
onChanged={option =>
|
||||
this.props.setLanguage(String(option.key))
|
||||
}
|
||||
style={{ width: 200 }}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
|
||||
|
@ -160,27 +190,30 @@ class AppTab extends React.Component<AppTabProps, AppTabState> {
|
|||
label={intl.get("app.theme")}
|
||||
options={this.themeChoices()}
|
||||
onChange={this.onThemeChange}
|
||||
selectedKey={this.state.themeSettings} />
|
||||
selectedKey={this.state.themeSettings}
|
||||
/>
|
||||
|
||||
<Label>{intl.get("app.fetchInterval")}</Label>
|
||||
<Stack horizontal>
|
||||
<Stack.Item>
|
||||
<Dropdown
|
||||
<Dropdown
|
||||
defaultSelectedKey={window.settings.getFetchInterval()}
|
||||
options={this.fetchIntervalOptions()}
|
||||
onChanged={this.onFetchIntervalChanged}
|
||||
style={{width: 200}} />
|
||||
style={{ width: 200 }}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
|
||||
<Label>{intl.get("searchEngine.name")}</Label>
|
||||
<Stack horizontal>
|
||||
<Stack.Item>
|
||||
<Dropdown
|
||||
<Dropdown
|
||||
defaultSelectedKey={window.settings.getSearchEngine()}
|
||||
options={this.searchEngineOptions()}
|
||||
onChanged={this.onSearchEngineChanged}
|
||||
style={{width: 200}} />
|
||||
style={{ width: 200 }}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
|
||||
|
@ -189,70 +222,100 @@ class AppTab extends React.Component<AppTabProps, AppTabState> {
|
|||
<Label>{intl.get("app.enableProxy")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Toggle checked={this.state.pacStatus} onChange={this.toggleStatus} />
|
||||
<Toggle
|
||||
checked={this.state.pacStatus}
|
||||
onChange={this.toggleStatus}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
{this.state.pacStatus && <form onSubmit={this.setUrl}>
|
||||
<Stack horizontal>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
required
|
||||
onGetErrorMessage={v => urlTest(v.trim()) ? "" : intl.get("app.badUrl")}
|
||||
placeholder={intl.get("app.pac")}
|
||||
name="pacUrl"
|
||||
onChange={this.handleInputChange}
|
||||
value={this.state.pacUrl} />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<DefaultButton
|
||||
disabled={!urlTest(this.state.pacUrl)}
|
||||
type="sumbit"
|
||||
text={intl.get("app.setPac")} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<span className="settings-hint up">
|
||||
{intl.get("app.pacHint")}
|
||||
</span>
|
||||
</form>}
|
||||
{this.state.pacStatus && (
|
||||
<form onSubmit={this.setUrl}>
|
||||
<Stack horizontal>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
required
|
||||
onGetErrorMessage={v =>
|
||||
urlTest(v.trim())
|
||||
? ""
|
||||
: intl.get("app.badUrl")
|
||||
}
|
||||
placeholder={intl.get("app.pac")}
|
||||
name="pacUrl"
|
||||
onChange={this.handleInputChange}
|
||||
value={this.state.pacUrl}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<DefaultButton
|
||||
disabled={!urlTest(this.state.pacUrl)}
|
||||
type="sumbit"
|
||||
text={intl.get("app.setPac")}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<span className="settings-hint up">
|
||||
{intl.get("app.pacHint")}
|
||||
</span>
|
||||
</form>
|
||||
)}
|
||||
|
||||
<Label>{intl.get("app.cleanup")}</Label>
|
||||
<Stack horizontal>
|
||||
<Stack.Item grow>
|
||||
<Dropdown
|
||||
placeholder={intl.get("app.deleteChoices")}
|
||||
<Dropdown
|
||||
placeholder={intl.get("app.deleteChoices")}
|
||||
options={this.deleteOptions()}
|
||||
selectedKey={this.state.deleteIndex}
|
||||
onChange={this.deleteChange} />
|
||||
onChange={this.deleteChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<DangerButton
|
||||
disabled={this.state.itemSize === null || this.state.deleteIndex === null}
|
||||
<DangerButton
|
||||
disabled={
|
||||
this.state.itemSize === null ||
|
||||
this.state.deleteIndex === null
|
||||
}
|
||||
text={intl.get("app.confirmDelete")}
|
||||
onClick={this.confirmDelete} />
|
||||
onClick={this.confirmDelete}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<span className="settings-hint up">
|
||||
{this.state.itemSize ? intl.get("app.itemSize", {size: this.state.itemSize}) : intl.get("app.calculatingSize")}
|
||||
{this.state.itemSize
|
||||
? intl.get("app.itemSize", { size: this.state.itemSize })
|
||||
: intl.get("app.calculatingSize")}
|
||||
</span>
|
||||
<Stack horizontal>
|
||||
<Stack.Item>
|
||||
<DefaultButton
|
||||
text={intl.get("app.cache")}
|
||||
disabled={this.state.cacheSize === null || this.state.cacheSize === "0MB"}
|
||||
onClick={this.clearCache} />
|
||||
disabled={
|
||||
this.state.cacheSize === null ||
|
||||
this.state.cacheSize === "0MB"
|
||||
}
|
||||
onClick={this.clearCache}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<span className="settings-hint up">
|
||||
{this.state.cacheSize ? intl.get("app.cacheSize", {size: this.state.cacheSize}) : intl.get("app.calculatingSize")}
|
||||
{this.state.cacheSize
|
||||
? intl.get("app.cacheSize", { size: this.state.cacheSize })
|
||||
: intl.get("app.calculatingSize")}
|
||||
</span>
|
||||
|
||||
<Label>{intl.get("app.data")}</Label>
|
||||
<Stack horizontal>
|
||||
<Stack.Item>
|
||||
<PrimaryButton onClick={exportAll} text={intl.get("app.backup")} />
|
||||
<Stack.Item>
|
||||
<PrimaryButton
|
||||
onClick={exportAll}
|
||||
text={intl.get("app.backup")}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<DefaultButton onClick={this.props.importAll} text={intl.get("app.restore")} />
|
||||
<DefaultButton
|
||||
onClick={this.props.importAll}
|
||||
text={intl.get("app.restore")}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</div>
|
||||
|
|
|
@ -2,8 +2,25 @@ import * as React from "react"
|
|||
import intl from "react-intl-universal"
|
||||
import { SourceGroup } from "../../schema-types"
|
||||
import { SourceState, RSSSource } from "../../scripts/models/source"
|
||||
import { IColumn, Selection, SelectionMode, DetailsList, Label, Stack,
|
||||
TextField, PrimaryButton, DefaultButton, Dropdown, IDropdownOption, CommandBarButton, MarqueeSelection, IDragDropEvents, MessageBar, MessageBarType, MessageBarButton } from "@fluentui/react"
|
||||
import {
|
||||
IColumn,
|
||||
Selection,
|
||||
SelectionMode,
|
||||
DetailsList,
|
||||
Label,
|
||||
Stack,
|
||||
TextField,
|
||||
PrimaryButton,
|
||||
DefaultButton,
|
||||
Dropdown,
|
||||
IDropdownOption,
|
||||
CommandBarButton,
|
||||
MarqueeSelection,
|
||||
IDragDropEvents,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
MessageBarButton,
|
||||
} from "@fluentui/react"
|
||||
import DangerButton from "../utils/danger-button"
|
||||
|
||||
type GroupsTabProps = {
|
||||
|
@ -20,10 +37,10 @@ type GroupsTabProps = {
|
|||
}
|
||||
|
||||
type GroupsTabState = {
|
||||
[formName: string]: any,
|
||||
selectedGroup: SourceGroup,
|
||||
selectedSources: RSSSource[],
|
||||
dropdownIndex: number,
|
||||
[formName: string]: any
|
||||
selectedGroup: SourceGroup
|
||||
selectedSources: RSSSource[]
|
||||
dropdownIndex: number
|
||||
manageGroup: boolean
|
||||
}
|
||||
|
||||
|
@ -45,30 +62,32 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
|
|||
selectedGroup: null,
|
||||
selectedSources: null,
|
||||
dropdownIndex: null,
|
||||
manageGroup: false
|
||||
manageGroup: false,
|
||||
}
|
||||
this.groupDragDropEvents = this.getGroupDragDropEvents()
|
||||
this.sourcesDragDropEvents = this.getSourcesDragDropEvents()
|
||||
this.groupSelection = new Selection({
|
||||
getKey: g => (g as SourceGroup).index,
|
||||
onSelectionChanged: () => {
|
||||
let g = this.groupSelection.getSelectedCount()
|
||||
? this.groupSelection.getSelection()[0] as SourceGroup : null
|
||||
let g = this.groupSelection.getSelectedCount()
|
||||
? (this.groupSelection.getSelection()[0] as SourceGroup)
|
||||
: null
|
||||
this.setState({
|
||||
selectedGroup: g,
|
||||
editGroupName: g && g.isMultiple ? g.name : ""
|
||||
editGroupName: g && g.isMultiple ? g.name : "",
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
this.sourcesSelection = new Selection({
|
||||
getKey: s => (s as RSSSource).sid,
|
||||
onSelectionChanged: () => {
|
||||
let sources = this.sourcesSelection.getSelectedCount()
|
||||
? this.sourcesSelection.getSelection() as RSSSource[] : null
|
||||
let sources = this.sourcesSelection.getSelectedCount()
|
||||
? (this.sourcesSelection.getSelection() as RSSSource[])
|
||||
: null
|
||||
this.setState({
|
||||
selectedSources: sources
|
||||
selectedSources: sources,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -79,9 +98,13 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
|
|||
minWidth: 40,
|
||||
maxWidth: 40,
|
||||
data: "string",
|
||||
onRender: (g: SourceGroup) => <>
|
||||
{g.isMultiple ? intl.get("groups.group") : intl.get("groups.source")}
|
||||
</>
|
||||
onRender: (g: SourceGroup) => (
|
||||
<>
|
||||
{g.isMultiple
|
||||
? intl.get("groups.group")
|
||||
: intl.get("groups.source")}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "capacity",
|
||||
|
@ -89,9 +112,9 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
|
|||
minWidth: 40,
|
||||
maxWidth: 60,
|
||||
data: "string",
|
||||
onRender: (g: SourceGroup) => <>
|
||||
{g.isMultiple ? g.sids.length : ""}
|
||||
</>
|
||||
onRender: (g: SourceGroup) => (
|
||||
<>{g.isMultiple ? g.sids.length : ""}</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "name",
|
||||
|
@ -99,10 +122,12 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
|
|||
minWidth: 200,
|
||||
data: "string",
|
||||
isRowHeader: true,
|
||||
onRender: (g: SourceGroup) => <>
|
||||
{g.isMultiple ? g.name : this.props.sources[g.sids[0]].name}
|
||||
</>
|
||||
}
|
||||
onRender: (g: SourceGroup) => (
|
||||
<>
|
||||
{g.isMultiple ? g.name : this.props.sources[g.sids[0]].name}
|
||||
</>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
sourceColumns: IColumn[] = [
|
||||
|
@ -114,25 +139,24 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
|
|||
iconName: "ImagePixel",
|
||||
minWidth: 16,
|
||||
maxWidth: 16,
|
||||
onRender: (s: RSSSource) => s.iconurl && (
|
||||
<img src={s.iconurl} className="favicon" />
|
||||
)
|
||||
onRender: (s: RSSSource) =>
|
||||
s.iconurl && <img src={s.iconurl} className="favicon" />,
|
||||
},
|
||||
{
|
||||
key: "name",
|
||||
name: intl.get("name"),
|
||||
fieldName: "name",
|
||||
minWidth: 200,
|
||||
data: 'string',
|
||||
isRowHeader: true
|
||||
data: "string",
|
||||
isRowHeader: true,
|
||||
},
|
||||
{
|
||||
key: "url",
|
||||
name: "URL",
|
||||
fieldName: "url",
|
||||
minWidth: 280,
|
||||
data: 'string'
|
||||
}
|
||||
data: "string",
|
||||
},
|
||||
]
|
||||
|
||||
getGroupDragDropEvents = (): IDragDropEvents => ({
|
||||
|
@ -154,13 +178,15 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
|
|||
})
|
||||
|
||||
reorderGroups = (item: SourceGroup) => {
|
||||
let draggedItem = this.groupSelection.isIndexSelected(this.groupDraggedIndex)
|
||||
? this.groupSelection.getSelection()[0] as SourceGroup
|
||||
: this.groupDraggedItem!
|
||||
|
||||
let draggedItem = this.groupSelection.isIndexSelected(
|
||||
this.groupDraggedIndex
|
||||
)
|
||||
? (this.groupSelection.getSelection()[0] as SourceGroup)
|
||||
: this.groupDraggedItem!
|
||||
|
||||
let insertIndex = item.index
|
||||
let groups = this.props.groups.filter(g => g.index != draggedItem.index)
|
||||
|
||||
|
||||
groups.splice(insertIndex, 0, draggedItem)
|
||||
this.groupSelection.setAllSelected(false)
|
||||
this.props.reorderGroups(groups)
|
||||
|
@ -185,15 +211,21 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
|
|||
})
|
||||
|
||||
reorderSources = (item: RSSSource) => {
|
||||
let draggedItems = this.sourcesSelection.isIndexSelected(this.sourcesDraggedIndex)
|
||||
? (this.sourcesSelection.getSelection() as RSSSource[]).map(s => s.sid)
|
||||
: [this.sourcesDraggedItem!.sid]
|
||||
|
||||
let draggedItems = this.sourcesSelection.isIndexSelected(
|
||||
this.sourcesDraggedIndex
|
||||
)
|
||||
? (this.sourcesSelection.getSelection() as RSSSource[]).map(
|
||||
s => s.sid
|
||||
)
|
||||
: [this.sourcesDraggedItem!.sid]
|
||||
|
||||
let insertIndex = this.state.selectedGroup.sids.indexOf(item.sid)
|
||||
let items = this.state.selectedGroup.sids.filter(sid => !draggedItems.includes(sid))
|
||||
|
||||
let items = this.state.selectedGroup.sids.filter(
|
||||
sid => !draggedItems.includes(sid)
|
||||
)
|
||||
|
||||
items.splice(insertIndex, 0, ...draggedItems)
|
||||
|
||||
|
||||
let group = { ...this.state.selectedGroup, sids: items }
|
||||
this.props.updateGroup(group)
|
||||
this.setState({ selectedGroup: group })
|
||||
|
@ -204,19 +236,22 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
|
|||
this.setState({
|
||||
selectedGroup: g,
|
||||
editGroupName: g && g.isMultiple ? g.name : "",
|
||||
manageGroup: true
|
||||
manageGroup: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
dropdownOptions = () => this.props.groups.filter(g => g.isMultiple).map(g => ({
|
||||
key: g.index,
|
||||
text: g.name
|
||||
}))
|
||||
dropdownOptions = () =>
|
||||
this.props.groups
|
||||
.filter(g => g.isMultiple)
|
||||
.map(g => ({
|
||||
key: g.index,
|
||||
text: g.name,
|
||||
}))
|
||||
|
||||
handleInputChange = (event) => {
|
||||
handleInputChange = event => {
|
||||
const name: string = event.target.name
|
||||
this.setState({[name]: event.target.value})
|
||||
this.setState({ [name]: event.target.value })
|
||||
}
|
||||
|
||||
validateNewGroupName = (v: string) => {
|
||||
|
@ -235,21 +270,32 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
|
|||
createGroup = (event: React.FormEvent) => {
|
||||
event.preventDefault()
|
||||
let trimmed = this.state.newGroupName.trim()
|
||||
if (this.validateNewGroupName(trimmed) === "") this.props.createGroup(trimmed)
|
||||
if (this.validateNewGroupName(trimmed) === "")
|
||||
this.props.createGroup(trimmed)
|
||||
}
|
||||
|
||||
addToGroup = () => {
|
||||
this.props.addToGroup(this.state.dropdownIndex, this.state.selectedGroup.sids[0])
|
||||
this.props.addToGroup(
|
||||
this.state.dropdownIndex,
|
||||
this.state.selectedGroup.sids[0]
|
||||
)
|
||||
}
|
||||
|
||||
removeFromGroup = () => {
|
||||
this.props.removeFromGroup(this.state.selectedGroup.index, this.state.selectedSources.map(s => s.sid))
|
||||
this.props.removeFromGroup(
|
||||
this.state.selectedGroup.index,
|
||||
this.state.selectedSources.map(s => s.sid)
|
||||
)
|
||||
this.setState({ selectedSources: null })
|
||||
}
|
||||
|
||||
deleteGroup = () => {
|
||||
this.props.deleteGroup(this.state.selectedGroup.index)
|
||||
this.groupSelection.setIndexSelected(this.state.selectedGroup.index, false, false)
|
||||
this.groupSelection.setIndexSelected(
|
||||
this.state.selectedGroup.index,
|
||||
false,
|
||||
false
|
||||
)
|
||||
this.setState({ selectedGroup: null })
|
||||
}
|
||||
|
||||
|
@ -265,126 +311,194 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
|
|||
|
||||
render = () => (
|
||||
<div className="tab-body">
|
||||
{this.state.manageGroup && this.state.selectedGroup &&
|
||||
<>
|
||||
<Stack horizontal horizontalAlign="space-between" style={{height: 40}}>
|
||||
<CommandBarButton
|
||||
text={intl.get("groups.exitGroup")}
|
||||
iconProps={{iconName: "BackToWindow"}}
|
||||
onClick={() => this.setState({manageGroup: false})} />
|
||||
{this.state.selectedSources != null && <CommandBarButton
|
||||
text={intl.get("groups.deleteSource")}
|
||||
onClick={this.removeFromGroup}
|
||||
iconProps={{iconName: "RemoveFromShoppingList", style: {color: "#d13438"}}} />}
|
||||
</Stack>
|
||||
|
||||
<MarqueeSelection selection={this.sourcesSelection} isDraggingConstrainedToRoot={true}>
|
||||
<DetailsList
|
||||
compact={true}
|
||||
items={this.state.selectedGroup.sids.map(sid => this.props.sources[sid])}
|
||||
columns={this.sourceColumns}
|
||||
dragDropEvents={this.sourcesDragDropEvents}
|
||||
setKey="multiple"
|
||||
selection={this.sourcesSelection}
|
||||
selectionMode={SelectionMode.multiple} />
|
||||
</MarqueeSelection>
|
||||
|
||||
<span className="settings-hint">{intl.get("groups.sourceHint")}</span>
|
||||
</>}
|
||||
{(!this.state.manageGroup || !this.state.selectedGroup)
|
||||
?<>
|
||||
{this.props.serviceOn && (
|
||||
<MessageBar
|
||||
messageBarType={MessageBarType.info}
|
||||
isMultiline={false}
|
||||
actions={<MessageBarButton text={intl.get("service.importGroups")} onClick={this.props.importGroups} />}>
|
||||
{intl.get("service.groupsWarning")}
|
||||
</MessageBar>
|
||||
)}
|
||||
<form onSubmit={this.createGroup}>
|
||||
<Label htmlFor="newGroupName">{intl.get("groups.create")}</Label>
|
||||
<Stack horizontal>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
onGetErrorMessage={this.validateNewGroupName}
|
||||
validateOnLoad={false}
|
||||
placeholder={intl.get("groups.enterName")}
|
||||
value={this.state.newGroupName}
|
||||
id="newGroupName"
|
||||
name="newGroupName"
|
||||
onChange={this.handleInputChange} />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<PrimaryButton
|
||||
disabled={this.validateNewGroupName(this.state.newGroupName) !== ""}
|
||||
type="sumbit"
|
||||
text={intl.get("create")} />
|
||||
</Stack.Item>
|
||||
{this.state.manageGroup && this.state.selectedGroup && (
|
||||
<>
|
||||
<Stack
|
||||
horizontal
|
||||
horizontalAlign="space-between"
|
||||
style={{ height: 40 }}>
|
||||
<CommandBarButton
|
||||
text={intl.get("groups.exitGroup")}
|
||||
iconProps={{ iconName: "BackToWindow" }}
|
||||
onClick={() =>
|
||||
this.setState({ manageGroup: false })
|
||||
}
|
||||
/>
|
||||
{this.state.selectedSources != null && (
|
||||
<CommandBarButton
|
||||
text={intl.get("groups.deleteSource")}
|
||||
onClick={this.removeFromGroup}
|
||||
iconProps={{
|
||||
iconName: "RemoveFromShoppingList",
|
||||
style: { color: "#d13438" },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</form>
|
||||
|
||||
<DetailsList
|
||||
compact={true}
|
||||
items={this.props.groups}
|
||||
columns={this.groupColumns()}
|
||||
setKey="selected"
|
||||
onItemInvoked={this.manageGroup}
|
||||
dragDropEvents={this.groupDragDropEvents}
|
||||
selection={this.groupSelection}
|
||||
selectionMode={SelectionMode.single} />
|
||||
<MarqueeSelection
|
||||
selection={this.sourcesSelection}
|
||||
isDraggingConstrainedToRoot={true}>
|
||||
<DetailsList
|
||||
compact={true}
|
||||
items={this.state.selectedGroup.sids.map(
|
||||
sid => this.props.sources[sid]
|
||||
)}
|
||||
columns={this.sourceColumns}
|
||||
dragDropEvents={this.sourcesDragDropEvents}
|
||||
setKey="multiple"
|
||||
selection={this.sourcesSelection}
|
||||
selectionMode={SelectionMode.multiple}
|
||||
/>
|
||||
</MarqueeSelection>
|
||||
|
||||
{this.state.selectedGroup
|
||||
? ( this.state.selectedGroup.isMultiple
|
||||
?<>
|
||||
<Label>{intl.get("groups.selectedGroup")}</Label>
|
||||
<span className="settings-hint">
|
||||
{intl.get("groups.sourceHint")}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{!this.state.manageGroup || !this.state.selectedGroup ? (
|
||||
<>
|
||||
{this.props.serviceOn && (
|
||||
<MessageBar
|
||||
messageBarType={MessageBarType.info}
|
||||
isMultiline={false}
|
||||
actions={
|
||||
<MessageBarButton
|
||||
text={intl.get("service.importGroups")}
|
||||
onClick={this.props.importGroups}
|
||||
/>
|
||||
}>
|
||||
{intl.get("service.groupsWarning")}
|
||||
</MessageBar>
|
||||
)}
|
||||
<form onSubmit={this.createGroup}>
|
||||
<Label htmlFor="newGroupName">
|
||||
{intl.get("groups.create")}
|
||||
</Label>
|
||||
<Stack horizontal>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
onGetErrorMessage={v => v.trim().length == 0 ? intl.get("emptyName") : ""}
|
||||
onGetErrorMessage={
|
||||
this.validateNewGroupName
|
||||
}
|
||||
validateOnLoad={false}
|
||||
placeholder={intl.get("groups.enterName")}
|
||||
value={this.state.editGroupName}
|
||||
name="editGroupName"
|
||||
onChange={this.handleInputChange} />
|
||||
value={this.state.newGroupName}
|
||||
id="newGroupName"
|
||||
name="newGroupName"
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<DefaultButton
|
||||
disabled={this.state.editGroupName.trim().length == 0}
|
||||
onClick={this.updateGroupName}
|
||||
text={intl.get("groups.editName")} />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<DangerButton
|
||||
key={this.state.selectedGroup.index}
|
||||
onClick={this.deleteGroup}
|
||||
text={intl.get("groups.deleteGroup")} />
|
||||
<PrimaryButton
|
||||
disabled={
|
||||
this.validateNewGroupName(
|
||||
this.state.newGroupName
|
||||
) !== ""
|
||||
}
|
||||
type="sumbit"
|
||||
text={intl.get("create")}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</>
|
||||
:<>
|
||||
<Label>{intl.get("groups.selectedSource")}</Label>
|
||||
<Stack horizontal>
|
||||
<Stack.Item grow>
|
||||
<Dropdown
|
||||
placeholder={intl.get("groups.chooseGroup")}
|
||||
selectedKey={this.state.dropdownIndex}
|
||||
options={this.dropdownOptions()}
|
||||
onChange={this.dropdownChange} />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<DefaultButton
|
||||
disabled={this.state.dropdownIndex === null}
|
||||
onClick={this.addToGroup}
|
||||
text={intl.get("groups.addToGroup")} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</>
|
||||
)
|
||||
: <span className="settings-hint">{intl.get("groups.groupHint")}</span>
|
||||
}
|
||||
</> : null}
|
||||
</form>
|
||||
|
||||
<DetailsList
|
||||
compact={true}
|
||||
items={this.props.groups}
|
||||
columns={this.groupColumns()}
|
||||
setKey="selected"
|
||||
onItemInvoked={this.manageGroup}
|
||||
dragDropEvents={this.groupDragDropEvents}
|
||||
selection={this.groupSelection}
|
||||
selectionMode={SelectionMode.single}
|
||||
/>
|
||||
|
||||
{this.state.selectedGroup ? (
|
||||
this.state.selectedGroup.isMultiple ? (
|
||||
<>
|
||||
<Label>
|
||||
{intl.get("groups.selectedGroup")}
|
||||
</Label>
|
||||
<Stack horizontal>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
onGetErrorMessage={v =>
|
||||
v.trim().length == 0
|
||||
? intl.get("emptyName")
|
||||
: ""
|
||||
}
|
||||
validateOnLoad={false}
|
||||
placeholder={intl.get(
|
||||
"groups.enterName"
|
||||
)}
|
||||
value={this.state.editGroupName}
|
||||
name="editGroupName"
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<DefaultButton
|
||||
disabled={
|
||||
this.state.editGroupName.trim()
|
||||
.length == 0
|
||||
}
|
||||
onClick={this.updateGroupName}
|
||||
text={intl.get("groups.editName")}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<DangerButton
|
||||
key={this.state.selectedGroup.index}
|
||||
onClick={this.deleteGroup}
|
||||
text={intl.get(
|
||||
"groups.deleteGroup"
|
||||
)}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Label>
|
||||
{intl.get("groups.selectedSource")}
|
||||
</Label>
|
||||
<Stack horizontal>
|
||||
<Stack.Item grow>
|
||||
<Dropdown
|
||||
placeholder={intl.get(
|
||||
"groups.chooseGroup"
|
||||
)}
|
||||
selectedKey={
|
||||
this.state.dropdownIndex
|
||||
}
|
||||
options={this.dropdownOptions()}
|
||||
onChange={this.dropdownChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<DefaultButton
|
||||
disabled={
|
||||
this.state.dropdownIndex ===
|
||||
null
|
||||
}
|
||||
onClick={this.addToGroup}
|
||||
text={intl.get("groups.addToGroup")}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
<span className="settings-hint">
|
||||
{intl.get("groups.groupHint")}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default GroupsTab
|
||||
export default GroupsTab
|
||||
|
|
|
@ -1,8 +1,27 @@
|
|||
import * as React from "react"
|
||||
import intl from "react-intl-universal"
|
||||
import { SourceState, RSSSource } from "../../scripts/models/source"
|
||||
import { Stack, Label, Dropdown, IDropdownOption, TextField, PrimaryButton, Icon, DropdownMenuItemType,
|
||||
DefaultButton, DetailsList, IColumn, CommandBar, ICommandBarItemProps, Selection, SelectionMode, MarqueeSelection, IDragDropEvents, Link, IIconProps } from "@fluentui/react"
|
||||
import {
|
||||
Stack,
|
||||
Label,
|
||||
Dropdown,
|
||||
IDropdownOption,
|
||||
TextField,
|
||||
PrimaryButton,
|
||||
Icon,
|
||||
DropdownMenuItemType,
|
||||
DefaultButton,
|
||||
DetailsList,
|
||||
IColumn,
|
||||
CommandBar,
|
||||
ICommandBarItemProps,
|
||||
Selection,
|
||||
SelectionMode,
|
||||
MarqueeSelection,
|
||||
IDragDropEvents,
|
||||
Link,
|
||||
IIconProps,
|
||||
} from "@fluentui/react"
|
||||
import { SourceRule, RuleActions } from "../../scripts/models/rule"
|
||||
import { FilterType } from "../../scripts/models/feed"
|
||||
import { validateRegex } from "../../scripts/utils"
|
||||
|
@ -60,13 +79,15 @@ class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
|
|||
mockTitle: "",
|
||||
mockCreator: "",
|
||||
mockContent: "",
|
||||
mockResult: ""
|
||||
mockResult: "",
|
||||
}
|
||||
this.rulesSelection = new Selection({
|
||||
getKey: (_, i) => i,
|
||||
onSelectionChanged: () => {
|
||||
this.setState({selectedRules: this.rulesSelection.getSelectedIndices()})
|
||||
}
|
||||
this.setState({
|
||||
selectedRules: this.rulesSelection.getSelectedIndices(),
|
||||
})
|
||||
},
|
||||
})
|
||||
this.rulesDragDropEvents = this.getRulesDragDropEvents()
|
||||
}
|
||||
|
@ -91,13 +112,15 @@ class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
|
|||
|
||||
reorderRules = (item: SourceRule) => {
|
||||
let rules = this.getSourceRules()
|
||||
let draggedItems = this.rulesSelection.isIndexSelected(this.rulesDraggedIndex)
|
||||
? this.rulesSelection.getSelection() as SourceRule[]
|
||||
: [this.rulesDraggedItem]
|
||||
|
||||
let draggedItems = this.rulesSelection.isIndexSelected(
|
||||
this.rulesDraggedIndex
|
||||
)
|
||||
? (this.rulesSelection.getSelection() as SourceRule[])
|
||||
: [this.rulesDraggedItem]
|
||||
|
||||
let insertIndex = rules.indexOf(item)
|
||||
let items = rules.filter(r => !draggedItems.includes(r))
|
||||
|
||||
|
||||
items.splice(insertIndex, 0, ...draggedItems)
|
||||
this.rulesSelection.setAllSelected(false)
|
||||
let source = this.props.sources[parseInt(this.state.sid)]
|
||||
|
@ -113,9 +136,11 @@ class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
|
|||
this.setState({
|
||||
regex: rule ? rule.filter.search : "",
|
||||
searchType: searchType,
|
||||
caseSensitive: rule ? !(rule.filter.type & FilterType.CaseInsensitive) : false,
|
||||
caseSensitive: rule
|
||||
? !(rule.filter.type & FilterType.CaseInsensitive)
|
||||
: false,
|
||||
match: rule ? rule.match : true,
|
||||
actionKeys: rule ? RuleActions.toKeys(rule.actions) : []
|
||||
actionKeys: rule ? RuleActions.toKeys(rule.actions) : [],
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -128,30 +153,34 @@ class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
|
|||
name: intl.get("rules.regex"),
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
onRender: (rule: SourceRule) => rule.filter.search
|
||||
onRender: (rule: SourceRule) => rule.filter.search,
|
||||
},
|
||||
{
|
||||
key: "actions",
|
||||
name: intl.get("rules.action"),
|
||||
minWidth: 100,
|
||||
onRender: (rule: SourceRule) => RuleActions.toKeys(rule.actions).map(k => intl.get(actionKeyMap[k])).join(", ")
|
||||
}
|
||||
onRender: (rule: SourceRule) =>
|
||||
RuleActions.toKeys(rule.actions)
|
||||
.map(k => intl.get(actionKeyMap[k]))
|
||||
.join(", "),
|
||||
},
|
||||
]
|
||||
|
||||
handleInputChange = (event) => {
|
||||
handleInputChange = event => {
|
||||
const name = event.target.name as "regex"
|
||||
this.setState({[name]: event.target.value})
|
||||
this.setState({ [name]: event.target.value })
|
||||
}
|
||||
|
||||
sourceOptions = (): IDropdownOption[] => Object.entries(this.props.sources).map(([sid, s]) => ({
|
||||
key: sid,
|
||||
text: s.name,
|
||||
data: { icon: s.iconurl }
|
||||
}))
|
||||
sourceOptions = (): IDropdownOption[] =>
|
||||
Object.entries(this.props.sources).map(([sid, s]) => ({
|
||||
key: sid,
|
||||
text: s.name,
|
||||
data: { icon: s.iconurl },
|
||||
}))
|
||||
onRenderSourceOption = (option: IDropdownOption) => (
|
||||
<div>
|
||||
{option.data && option.data.icon && (
|
||||
<img src={option.data.icon} className="favicon dropdown"/>
|
||||
<img src={option.data.icon} className="favicon dropdown" />
|
||||
)}
|
||||
<span>{option.text}</span>
|
||||
</div>
|
||||
|
@ -162,9 +191,14 @@ class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
|
|||
onSourceOptionChange = (_, item: IDropdownOption) => {
|
||||
this.initRuleEdit()
|
||||
this.rulesSelection.setAllSelected(false)
|
||||
this.setState({
|
||||
sid: item.key as string, selectedRules: [], editIndex: -1,
|
||||
mockTitle: "", mockCreator: "", mockContent: "", mockResult: ""
|
||||
this.setState({
|
||||
sid: item.key as string,
|
||||
selectedRules: [],
|
||||
editIndex: -1,
|
||||
mockTitle: "",
|
||||
mockCreator: "",
|
||||
mockContent: "",
|
||||
mockResult: "",
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -179,38 +213,51 @@ class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
|
|||
|
||||
matchOptions = (): IDropdownOption[] => [
|
||||
{ key: 1, text: intl.get("rules.match") },
|
||||
{ key: 0, text: intl.get("rules.notMatch") }
|
||||
{ key: 0, text: intl.get("rules.notMatch") },
|
||||
]
|
||||
onMatchOptionChange = (_, item: IDropdownOption) => {
|
||||
this.setState({ match: Boolean(item.key) })
|
||||
}
|
||||
|
||||
actionOptions = (): IDropdownOption[] => [
|
||||
...Object.entries(actionKeyMap).map(([k, t], i) => {
|
||||
if (k.includes("-false")) {
|
||||
return [{ key: k, text: intl.get(t) }, { key: i, text: "-", itemType: DropdownMenuItemType.Divider }]
|
||||
} else {
|
||||
return [{ key: k, text: intl.get(t) }]
|
||||
}
|
||||
})
|
||||
].flat(1)
|
||||
actionOptions = (): IDropdownOption[] =>
|
||||
[
|
||||
...Object.entries(actionKeyMap).map(([k, t], i) => {
|
||||
if (k.includes("-false")) {
|
||||
return [
|
||||
{ key: k, text: intl.get(t) },
|
||||
{
|
||||
key: i,
|
||||
text: "-",
|
||||
itemType: DropdownMenuItemType.Divider,
|
||||
},
|
||||
]
|
||||
} else {
|
||||
return [{ key: k, text: intl.get(t) }]
|
||||
}
|
||||
}),
|
||||
].flat(1)
|
||||
|
||||
onActionOptionChange = (_, item: IDropdownOption) => {
|
||||
if (item.selected) {
|
||||
this.setState(prevState => {
|
||||
let [a, f] = (item.key as string).split("-")
|
||||
let keys = prevState.actionKeys.filter(k => !k.startsWith(`${a}-`))
|
||||
let keys = prevState.actionKeys.filter(
|
||||
k => !k.startsWith(`${a}-`)
|
||||
)
|
||||
keys.push(item.key as string)
|
||||
return { actionKeys: keys }
|
||||
})
|
||||
} else {
|
||||
this.setState(prevState => ({ actionKeys: prevState.actionKeys.filter(k => k !== item.key) }))
|
||||
this.setState(prevState => ({
|
||||
actionKeys: prevState.actionKeys.filter(k => k !== item.key),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
validateRegexField = (value: string) => {
|
||||
if (value.length === 0) return intl.get("emptyField")
|
||||
else if (validateRegex(value) === null) return intl.get("rules.badRegex")
|
||||
else if (validateRegex(value) === null)
|
||||
return intl.get("rules.badRegex")
|
||||
else return ""
|
||||
}
|
||||
|
||||
|
@ -218,10 +265,16 @@ class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
|
|||
let filterType = FilterType.Default | FilterType.ShowHidden
|
||||
if (!this.state.caseSensitive) filterType |= FilterType.CaseInsensitive
|
||||
if (this.state.searchType === 1) filterType |= FilterType.FullSearch
|
||||
else if (this.state.searchType === 2) filterType |= FilterType.CreatorSearch
|
||||
let rule = new SourceRule(this.state.regex, this.state.actionKeys, filterType, this.state.match)
|
||||
else if (this.state.searchType === 2)
|
||||
filterType |= FilterType.CreatorSearch
|
||||
let rule = new SourceRule(
|
||||
this.state.regex,
|
||||
this.state.actionKeys,
|
||||
filterType,
|
||||
this.state.match
|
||||
)
|
||||
let source = this.props.sources[parseInt(this.state.sid)]
|
||||
let rules = source.rules ? [ ...source.rules ] : []
|
||||
let rules = source.rules ? [...source.rules] : []
|
||||
if (this.state.editIndex === -1) {
|
||||
rules.push(rule)
|
||||
} else {
|
||||
|
@ -243,28 +296,39 @@ class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
|
|||
let rules = this.getSourceRules()
|
||||
for (let i of this.state.selectedRules) rules[i] = null
|
||||
let source = this.props.sources[parseInt(this.state.sid)]
|
||||
this.props.updateSourceRules(source, rules.filter(r => r !== null))
|
||||
this.props.updateSourceRules(
|
||||
source,
|
||||
rules.filter(r => r !== null)
|
||||
)
|
||||
this.initRuleEdit()
|
||||
}
|
||||
|
||||
commandBarItems = (): ICommandBarItemProps[] => [{
|
||||
key: "new", text: intl.get("rules.new"), iconProps: { iconName: "Add" },
|
||||
onClick: this.newRule
|
||||
}]
|
||||
commandBarItems = (): ICommandBarItemProps[] => [
|
||||
{
|
||||
key: "new",
|
||||
text: intl.get("rules.new"),
|
||||
iconProps: { iconName: "Add" },
|
||||
onClick: this.newRule,
|
||||
},
|
||||
]
|
||||
commandBarFarItems = (): ICommandBarItemProps[] => {
|
||||
let items = []
|
||||
if (this.state.selectedRules.length === 1) {
|
||||
let index = this.state.selectedRules[0]
|
||||
items.push({
|
||||
key: "edit", text: intl.get("edit"), iconProps: { iconName: "Edit" },
|
||||
onClick: () => this.editRule(this.getSourceRules()[index], index)
|
||||
items.push({
|
||||
key: "edit",
|
||||
text: intl.get("edit"),
|
||||
iconProps: { iconName: "Edit" },
|
||||
onClick: () =>
|
||||
this.editRule(this.getSourceRules()[index], index),
|
||||
})
|
||||
}
|
||||
if (this.state.selectedRules.length > 0) {
|
||||
items.push({
|
||||
key: "del", text: intl.get("delete"),
|
||||
items.push({
|
||||
key: "del",
|
||||
text: intl.get("delete"),
|
||||
iconProps: { iconName: "Delete", style: { color: "#d13438" } },
|
||||
onClick: this.deleteRules
|
||||
onClick: this.deleteRules,
|
||||
})
|
||||
}
|
||||
return items
|
||||
|
@ -278,7 +342,9 @@ class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
|
|||
item.creator = this.state.mockCreator
|
||||
SourceRule.applyAll(this.getSourceRules(), item)
|
||||
let result = []
|
||||
result.push(intl.get(item.hasRead ? "article.markRead" : "article.markUnread"))
|
||||
result.push(
|
||||
intl.get(item.hasRead ? "article.markRead" : "article.markUnread")
|
||||
)
|
||||
if (item.starred) result.push(intl.get("article.star"))
|
||||
if (item.hidden) result.push(intl.get("article.hide"))
|
||||
if (item.notify) result.push(intl.get("article.notify"))
|
||||
|
@ -291,20 +357,22 @@ class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
|
|||
regexCaseIconProps = (): IIconProps => ({
|
||||
title: intl.get("context.caseSensitive"),
|
||||
children: "Aa",
|
||||
style: {
|
||||
fontSize: 12,
|
||||
style: {
|
||||
fontSize: 12,
|
||||
fontStyle: "normal",
|
||||
cursor: "pointer",
|
||||
pointerEvents: "unset",
|
||||
color: this.state.caseSensitive ? "var(--black)" : "var(--neutralTertiary)",
|
||||
color: this.state.caseSensitive
|
||||
? "var(--black)"
|
||||
: "var(--neutralTertiary)",
|
||||
textDecoration: this.state.caseSensitive ? "underline" : "",
|
||||
},
|
||||
onClick: this.toggleCaseSensitivity
|
||||
onClick: this.toggleCaseSensitivity,
|
||||
})
|
||||
|
||||
render = () => (
|
||||
<div className="tab-body">
|
||||
<Stack horizontal tokens={{childrenGap: 16}}>
|
||||
<Stack horizontal tokens={{ childrenGap: 16 }}>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("rules.source")}</Label>
|
||||
</Stack.Item>
|
||||
|
@ -315,124 +383,169 @@ class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
|
|||
onRenderOption={this.onRenderSourceOption}
|
||||
onRenderTitle={this.onRenderSourceTitle}
|
||||
selectedKey={this.state.sid}
|
||||
onChange={this.onSourceOptionChange} />
|
||||
onChange={this.onSourceOptionChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
|
||||
{this.state.sid
|
||||
? (this.state.editIndex > -1 || !this.getSourceRules() || this.getSourceRules().length === 0
|
||||
? <>
|
||||
<Label>
|
||||
{intl.get((this.state.editIndex >= 0 && this.state.editIndex < this.getSourceRules().length) ? "edit" : "rules.new")}
|
||||
</Label>
|
||||
<Stack horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("rules.if")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Dropdown
|
||||
options={this.searchOptions()}
|
||||
selectedKey={this.state.searchType}
|
||||
onChange={this.onSearchOptionChange}
|
||||
style={{width: 140}} />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Dropdown
|
||||
options={this.matchOptions()}
|
||||
selectedKey={this.state.match ? 1 : 0}
|
||||
onChange={this.onMatchOptionChange}
|
||||
style={{width: 130}} />
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
name="regex"
|
||||
placeholder={intl.get("rules.regex")}
|
||||
iconProps={this.regexCaseIconProps()}
|
||||
value={this.state.regex}
|
||||
onGetErrorMessage={this.validateRegexField}
|
||||
validateOnLoad={false}
|
||||
onChange={this.handleInputChange} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("rules.then")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<Dropdown multiSelect
|
||||
placeholder={intl.get("rules.selectAction")}
|
||||
options={this.actionOptions()}
|
||||
selectedKeys={this.state.actionKeys}
|
||||
onChange={this.onActionOptionChange}
|
||||
onRenderCaretDown={() => <Icon iconName="CirclePlus" />} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack horizontal>
|
||||
<Stack.Item>
|
||||
<PrimaryButton
|
||||
disabled={this.state.regex.length == 0 || validateRegex(this.state.regex) === null || this.state.actionKeys.length == 0}
|
||||
text={intl.get("confirm")}
|
||||
onClick={this.saveRule} />
|
||||
</Stack.Item>
|
||||
{this.state.editIndex > -1 && <Stack.Item>
|
||||
<DefaultButton
|
||||
text={intl.get("cancel")}
|
||||
onClick={() => this.setState({ editIndex: -1 })} />
|
||||
</Stack.Item>}
|
||||
</Stack>
|
||||
</>
|
||||
: <>
|
||||
<CommandBar
|
||||
items={this.commandBarItems()}
|
||||
farItems={this.commandBarFarItems()} />
|
||||
<MarqueeSelection selection={this.rulesSelection} isDraggingConstrainedToRoot>
|
||||
<DetailsList compact
|
||||
columns={this.ruleColumns()}
|
||||
items={this.getSourceRules()}
|
||||
onItemInvoked={this.editRule}
|
||||
dragDropEvents={this.rulesDragDropEvents}
|
||||
setKey="selected"
|
||||
{this.state.sid ? (
|
||||
this.state.editIndex > -1 ||
|
||||
!this.getSourceRules() ||
|
||||
this.getSourceRules().length === 0 ? (
|
||||
<>
|
||||
<Label>
|
||||
{intl.get(
|
||||
this.state.editIndex >= 0 &&
|
||||
this.state.editIndex <
|
||||
this.getSourceRules().length
|
||||
? "edit"
|
||||
: "rules.new"
|
||||
)}
|
||||
</Label>
|
||||
<Stack horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("rules.if")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Dropdown
|
||||
options={this.searchOptions()}
|
||||
selectedKey={this.state.searchType}
|
||||
onChange={this.onSearchOptionChange}
|
||||
style={{ width: 140 }}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Dropdown
|
||||
options={this.matchOptions()}
|
||||
selectedKey={this.state.match ? 1 : 0}
|
||||
onChange={this.onMatchOptionChange}
|
||||
style={{ width: 130 }}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
name="regex"
|
||||
placeholder={intl.get("rules.regex")}
|
||||
iconProps={this.regexCaseIconProps()}
|
||||
value={this.state.regex}
|
||||
onGetErrorMessage={this.validateRegexField}
|
||||
validateOnLoad={false}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("rules.then")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<Dropdown
|
||||
multiSelect
|
||||
placeholder={intl.get("rules.selectAction")}
|
||||
options={this.actionOptions()}
|
||||
selectedKeys={this.state.actionKeys}
|
||||
onChange={this.onActionOptionChange}
|
||||
onRenderCaretDown={() => (
|
||||
<Icon iconName="CirclePlus" />
|
||||
)}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack horizontal>
|
||||
<Stack.Item>
|
||||
<PrimaryButton
|
||||
disabled={
|
||||
this.state.regex.length == 0 ||
|
||||
validateRegex(this.state.regex) ===
|
||||
null ||
|
||||
this.state.actionKeys.length == 0
|
||||
}
|
||||
text={intl.get("confirm")}
|
||||
onClick={this.saveRule}
|
||||
/>
|
||||
</Stack.Item>
|
||||
{this.state.editIndex > -1 && (
|
||||
<Stack.Item>
|
||||
<DefaultButton
|
||||
text={intl.get("cancel")}
|
||||
onClick={() =>
|
||||
this.setState({ editIndex: -1 })
|
||||
}
|
||||
/>
|
||||
</Stack.Item>
|
||||
)}
|
||||
</Stack>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CommandBar
|
||||
items={this.commandBarItems()}
|
||||
farItems={this.commandBarFarItems()}
|
||||
/>
|
||||
<MarqueeSelection
|
||||
selection={this.rulesSelection}
|
||||
selectionMode={SelectionMode.multiple} />
|
||||
</MarqueeSelection>
|
||||
<span className="settings-hint up">{intl.get("rules.hint")}</span>
|
||||
isDraggingConstrainedToRoot>
|
||||
<DetailsList
|
||||
compact
|
||||
columns={this.ruleColumns()}
|
||||
items={this.getSourceRules()}
|
||||
onItemInvoked={this.editRule}
|
||||
dragDropEvents={this.rulesDragDropEvents}
|
||||
setKey="selected"
|
||||
selection={this.rulesSelection}
|
||||
selectionMode={SelectionMode.multiple}
|
||||
/>
|
||||
</MarqueeSelection>
|
||||
<span className="settings-hint up">
|
||||
{intl.get("rules.hint")}
|
||||
</span>
|
||||
|
||||
<Label>{intl.get("rules.test")}</Label>
|
||||
<Stack horizontal>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
name="mockTitle"
|
||||
placeholder={intl.get("rules.title")}
|
||||
value={this.state.mockTitle}
|
||||
onChange={this.handleInputChange} />
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
name="mockCreator"
|
||||
placeholder={intl.get("rules.creator")}
|
||||
value={this.state.mockCreator}
|
||||
onChange={this.handleInputChange} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack horizontal>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
name="mockContent"
|
||||
placeholder={intl.get("rules.content")}
|
||||
value={this.state.mockContent}
|
||||
onChange={this.handleInputChange} />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<PrimaryButton
|
||||
text={intl.get("confirm")}
|
||||
onClick={this.testMockItem} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<span className="settings-hint up">{this.state.mockResult}</span>
|
||||
</>)
|
||||
: (
|
||||
<Stack horizontalAlign="center" style={{marginTop: 64}}>
|
||||
<Stack className="settings-rules-icons" horizontal tokens={{childrenGap: 12}}>
|
||||
<Label>{intl.get("rules.test")}</Label>
|
||||
<Stack horizontal>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
name="mockTitle"
|
||||
placeholder={intl.get("rules.title")}
|
||||
value={this.state.mockTitle}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
name="mockCreator"
|
||||
placeholder={intl.get("rules.creator")}
|
||||
value={this.state.mockCreator}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack horizontal>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
name="mockContent"
|
||||
placeholder={intl.get("rules.content")}
|
||||
value={this.state.mockContent}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<PrimaryButton
|
||||
text={intl.get("confirm")}
|
||||
onClick={this.testMockItem}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<span className="settings-hint up">
|
||||
{this.state.mockResult}
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
<Stack horizontalAlign="center" style={{ marginTop: 64 }}>
|
||||
<Stack
|
||||
className="settings-rules-icons"
|
||||
horizontal
|
||||
tokens={{ childrenGap: 12 }}>
|
||||
<Icon iconName="Filter" />
|
||||
<Icon iconName="FavoriteStar" />
|
||||
<Icon iconName="Ringer" />
|
||||
|
@ -440,9 +553,13 @@ class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
|
|||
</Stack>
|
||||
<span className="settings-hint">
|
||||
{intl.get("rules.intro")}
|
||||
<Link
|
||||
onClick={() => window.utils.openExternal("https://github.com/yang991178/fluent-reader/wiki/Support#rules")}
|
||||
style={{marginLeft: 6}}>
|
||||
<Link
|
||||
onClick={() =>
|
||||
window.utils.openExternal(
|
||||
"https://github.com/yang991178/fluent-reader/wiki/Support#rules"
|
||||
)
|
||||
}
|
||||
style={{ marginLeft: 6 }}>
|
||||
{intl.get("rules.help")}
|
||||
</Link>
|
||||
</span>
|
||||
|
@ -452,4 +569,4 @@ class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
|
|||
)
|
||||
}
|
||||
|
||||
export default RulesTab
|
||||
export default RulesTab
|
||||
|
|
|
@ -25,11 +25,14 @@ type ServiceTabState = {
|
|||
type: SyncService
|
||||
}
|
||||
|
||||
export class ServiceTab extends React.Component<ServiceTabProps, ServiceTabState> {
|
||||
export class ServiceTab extends React.Component<
|
||||
ServiceTabProps,
|
||||
ServiceTabState
|
||||
> {
|
||||
constructor(props: ServiceTabProps) {
|
||||
super(props)
|
||||
this.state = {
|
||||
type: props.configs.type
|
||||
type: props.configs.type,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,7 +46,9 @@ export class ServiceTab extends React.Component<ServiceTabProps, ServiceTabState
|
|||
|
||||
onServiceOptionChange = (_, option: IDropdownOption) => {
|
||||
if (option.key === -1) {
|
||||
window.utils.openExternal("https://github.com/yang991178/fluent-reader/issues/23")
|
||||
window.utils.openExternal(
|
||||
"https://github.com/yang991178/fluent-reader/issues/23"
|
||||
)
|
||||
} else {
|
||||
this.setState({ type: option.key as number })
|
||||
}
|
||||
|
@ -55,29 +60,60 @@ export class ServiceTab extends React.Component<ServiceTabProps, ServiceTabState
|
|||
|
||||
getConfigsTab = () => {
|
||||
switch (this.state.type) {
|
||||
case SyncService.Fever: return <FeverConfigsTab {...this.props} exit={this.exitConfigsTab} />
|
||||
case SyncService.Feedbin: return <FeedbinConfigsTab {...this.props} exit={this.exitConfigsTab} />
|
||||
case SyncService.GReader: return <GReaderConfigsTab {...this.props} exit={this.exitConfigsTab} />
|
||||
case SyncService.Inoreader: return <InoreaderConfigsTab {...this.props} exit={this.exitConfigsTab} />
|
||||
default: return null
|
||||
case SyncService.Fever:
|
||||
return (
|
||||
<FeverConfigsTab
|
||||
{...this.props}
|
||||
exit={this.exitConfigsTab}
|
||||
/>
|
||||
)
|
||||
case SyncService.Feedbin:
|
||||
return (
|
||||
<FeedbinConfigsTab
|
||||
{...this.props}
|
||||
exit={this.exitConfigsTab}
|
||||
/>
|
||||
)
|
||||
case SyncService.GReader:
|
||||
return (
|
||||
<GReaderConfigsTab
|
||||
{...this.props}
|
||||
exit={this.exitConfigsTab}
|
||||
/>
|
||||
)
|
||||
case SyncService.Inoreader:
|
||||
return (
|
||||
<InoreaderConfigsTab
|
||||
{...this.props}
|
||||
exit={this.exitConfigsTab}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
render = () => (
|
||||
<div className="tab-body">
|
||||
{this.state.type === SyncService.None
|
||||
? (
|
||||
<Stack horizontalAlign="center" style={{marginTop: 64}}>
|
||||
<Stack className="settings-rules-icons" horizontal tokens={{childrenGap: 12}}>
|
||||
{this.state.type === SyncService.None ? (
|
||||
<Stack horizontalAlign="center" style={{ marginTop: 64 }}>
|
||||
<Stack
|
||||
className="settings-rules-icons"
|
||||
horizontal
|
||||
tokens={{ childrenGap: 12 }}>
|
||||
<Icon iconName="ThisPC" />
|
||||
<Icon iconName="Sync" />
|
||||
<Icon iconName="Cloud" />
|
||||
</Stack>
|
||||
<span className="settings-hint">
|
||||
{intl.get("service.intro")}
|
||||
<Link
|
||||
onClick={() => window.utils.openExternal("https://github.com/yang991178/fluent-reader/wiki/Support#services")}
|
||||
style={{marginLeft: 6}}>
|
||||
<Link
|
||||
onClick={() =>
|
||||
window.utils.openExternal(
|
||||
"https://github.com/yang991178/fluent-reader/wiki/Support#services"
|
||||
)
|
||||
}
|
||||
style={{ marginLeft: 6 }}>
|
||||
{intl.get("rules.help")}
|
||||
</Link>
|
||||
</span>
|
||||
|
@ -86,10 +122,12 @@ export class ServiceTab extends React.Component<ServiceTabProps, ServiceTabState
|
|||
options={this.serviceOptions()}
|
||||
selectedKey={null}
|
||||
onChange={this.onServiceOptionChange}
|
||||
style={{marginTop: 32, width: 180}} />
|
||||
style={{ marginTop: 32, width: 180 }}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
: this.getConfigsTab()}
|
||||
) : (
|
||||
this.getConfigsTab()
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,19 @@ import intl from "react-intl-universal"
|
|||
import { ServiceConfigsTabProps } from "../service"
|
||||
import { FeedbinConfigs } from "../../../scripts/models/services/feedbin"
|
||||
import { SyncService } from "../../../schema-types"
|
||||
import { Stack, Icon, Label, TextField, PrimaryButton, DefaultButton, Checkbox,
|
||||
MessageBar, MessageBarType, Dropdown, IDropdownOption } from "@fluentui/react"
|
||||
import {
|
||||
Stack,
|
||||
Icon,
|
||||
Label,
|
||||
TextField,
|
||||
PrimaryButton,
|
||||
DefaultButton,
|
||||
Checkbox,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
Dropdown,
|
||||
IDropdownOption,
|
||||
} from "@fluentui/react"
|
||||
import DangerButton from "../../utils/danger-button"
|
||||
import { urlTest } from "../../../scripts/utils"
|
||||
import LiteExporter from "./lite-exporter"
|
||||
|
@ -18,7 +29,10 @@ type FeedbinConfigsTabState = {
|
|||
importGroups: boolean
|
||||
}
|
||||
|
||||
class FeedbinConfigsTab extends React.Component<ServiceConfigsTabProps, FeedbinConfigsTabState> {
|
||||
class FeedbinConfigsTab extends React.Component<
|
||||
ServiceConfigsTabProps,
|
||||
FeedbinConfigsTabState
|
||||
> {
|
||||
constructor(props: ServiceConfigsTabProps) {
|
||||
super(props)
|
||||
const configs = props.configs as FeedbinConfigs
|
||||
|
@ -38,24 +52,33 @@ class FeedbinConfigsTab extends React.Component<ServiceConfigsTabProps, FeedbinC
|
|||
{ key: 750, text: intl.get("service.fetchLimitNum", { count: 750 }) },
|
||||
{ key: 1000, text: intl.get("service.fetchLimitNum", { count: 1000 }) },
|
||||
{ key: 1500, text: intl.get("service.fetchLimitNum", { count: 1500 }) },
|
||||
{ key: Number.MAX_SAFE_INTEGER, text: intl.get("service.fetchUnlimited") },
|
||||
{
|
||||
key: Number.MAX_SAFE_INTEGER,
|
||||
text: intl.get("service.fetchUnlimited"),
|
||||
},
|
||||
]
|
||||
onFetchLimitOptionChange = (_, option: IDropdownOption) => {
|
||||
this.setState({ fetchLimit: option.key as number })
|
||||
}
|
||||
|
||||
handleInputChange = (event) => {
|
||||
handleInputChange = event => {
|
||||
const name: string = event.target.name
|
||||
// @ts-expect-error
|
||||
this.setState({[name]: event.target.value})
|
||||
this.setState({ [name]: event.target.value })
|
||||
}
|
||||
|
||||
checkNotEmpty = (v: string) => {
|
||||
return (!this.state.existing && v.length == 0) ? intl.get("emptyField") : ""
|
||||
return !this.state.existing && v.length == 0
|
||||
? intl.get("emptyField")
|
||||
: ""
|
||||
}
|
||||
|
||||
validateForm = () => {
|
||||
return urlTest(this.state.endpoint.trim()) && (this.state.existing || (this.state.username && this.state.password))
|
||||
return (
|
||||
urlTest(this.state.endpoint.trim()) &&
|
||||
(this.state.existing ||
|
||||
(this.state.username && this.state.password))
|
||||
)
|
||||
}
|
||||
|
||||
save = async () => {
|
||||
|
@ -64,10 +87,9 @@ class FeedbinConfigsTab extends React.Component<ServiceConfigsTabProps, FeedbinC
|
|||
configs = {
|
||||
...this.props.configs,
|
||||
endpoint: this.state.endpoint,
|
||||
fetchLimit: this.state.fetchLimit
|
||||
fetchLimit: this.state.fetchLimit,
|
||||
} as FeedbinConfigs
|
||||
if (this.state.password)
|
||||
configs.password = this.state.password
|
||||
if (this.state.password) configs.password = this.state.password
|
||||
} else {
|
||||
configs = {
|
||||
type: SyncService.Feedbin,
|
||||
|
@ -86,7 +108,10 @@ class FeedbinConfigsTab extends React.Component<ServiceConfigsTabProps, FeedbinC
|
|||
this.props.sync()
|
||||
} else {
|
||||
this.props.blockActions()
|
||||
window.utils.showErrorBox(intl.get("service.failure"), intl.get("service.failureHint"))
|
||||
window.utils.showErrorBox(
|
||||
intl.get("service.failure"),
|
||||
intl.get("service.failureHint")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,88 +121,133 @@ class FeedbinConfigsTab extends React.Component<ServiceConfigsTabProps, FeedbinC
|
|||
}
|
||||
|
||||
render() {
|
||||
return <>
|
||||
{!this.state.existing && (
|
||||
<MessageBar messageBarType={MessageBarType.warning}>{intl.get("service.overwriteWarning")}</MessageBar>
|
||||
)}
|
||||
<Stack horizontalAlign="center" style={{marginTop: 48}}>
|
||||
<svg style={{fill: "var(--black)", width: 32, userSelect: "none"}} viewBox="0 0 120 120"><path d="M116.4,87.2c-22.5-0.1-96.9-0.1-112.4,0c-4.9,0-4.8-22.5,0-23.3c15.6-2.5,60.3,0,60.3,0s16.1,16.3,20.8,16.3 c4.8,0,16.1-16.3,16.1-16.3s12.8-2.3,15.2,0C120.3,67.9,121.2,87.3,116.4,87.2z" /><path d="M110.9,108.8L110.9,108.8c-19.1,2.5-83.6,1.9-103,0c-4.3-0.4-1.5-13.6-1.5-13.6h108.1 C114.4,95.2,116.3,108.1,110.9,108.8z" /><path d="M58.1,9.9C30.6,6.2,7.9,29.1,7.9,51.3l102.6,1C110.6,30.2,85.4,13.6,58.1,9.9z" /></svg>
|
||||
<Label style={{margin: "8px 0 36px"}}>Feedbin</Label>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("service.endpoint")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
onGetErrorMessage={v => urlTest(v.trim()) ? "" : intl.get("sources.badUrl")}
|
||||
validateOnLoad={false}
|
||||
name="endpoint"
|
||||
value={this.state.endpoint}
|
||||
onChange={this.handleInputChange} />
|
||||
</Stack.Item>
|
||||
return (
|
||||
<>
|
||||
{!this.state.existing && (
|
||||
<MessageBar messageBarType={MessageBarType.warning}>
|
||||
{intl.get("service.overwriteWarning")}
|
||||
</MessageBar>
|
||||
)}
|
||||
<Stack horizontalAlign="center" style={{ marginTop: 48 }}>
|
||||
<svg
|
||||
style={{
|
||||
fill: "var(--black)",
|
||||
width: 32,
|
||||
userSelect: "none",
|
||||
}}
|
||||
viewBox="0 0 120 120">
|
||||
<path d="M116.4,87.2c-22.5-0.1-96.9-0.1-112.4,0c-4.9,0-4.8-22.5,0-23.3c15.6-2.5,60.3,0,60.3,0s16.1,16.3,20.8,16.3 c4.8,0,16.1-16.3,16.1-16.3s12.8-2.3,15.2,0C120.3,67.9,121.2,87.3,116.4,87.2z" />
|
||||
<path d="M110.9,108.8L110.9,108.8c-19.1,2.5-83.6,1.9-103,0c-4.3-0.4-1.5-13.6-1.5-13.6h108.1 C114.4,95.2,116.3,108.1,110.9,108.8z" />
|
||||
<path d="M58.1,9.9C30.6,6.2,7.9,29.1,7.9,51.3l102.6,1C110.6,30.2,85.4,13.6,58.1,9.9z" />
|
||||
</svg>
|
||||
<Label style={{ margin: "8px 0 36px" }}>Feedbin</Label>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("service.endpoint")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
onGetErrorMessage={v =>
|
||||
urlTest(v.trim())
|
||||
? ""
|
||||
: intl.get("sources.badUrl")
|
||||
}
|
||||
validateOnLoad={false}
|
||||
name="endpoint"
|
||||
value={this.state.endpoint}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>Email</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
disabled={this.state.existing}
|
||||
onGetErrorMessage={this.checkNotEmpty}
|
||||
validateOnLoad={false}
|
||||
name="username"
|
||||
value={this.state.username}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("service.password")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
type="password"
|
||||
placeholder={
|
||||
this.state.existing
|
||||
? intl.get("service.unchanged")
|
||||
: ""
|
||||
}
|
||||
onGetErrorMessage={this.checkNotEmpty}
|
||||
validateOnLoad={false}
|
||||
name="password"
|
||||
value={this.state.password}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("service.fetchLimit")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<Dropdown
|
||||
options={this.fetchLimitOptions()}
|
||||
selectedKey={this.state.fetchLimit}
|
||||
onChange={this.onFetchLimitOptionChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
{!this.state.existing && (
|
||||
<Checkbox
|
||||
label={intl.get("service.importGroups")}
|
||||
checked={this.state.importGroups}
|
||||
onChange={(_, c) =>
|
||||
this.setState({ importGroups: c })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Stack horizontal style={{ marginTop: 32 }}>
|
||||
<Stack.Item>
|
||||
<PrimaryButton
|
||||
disabled={!this.validateForm()}
|
||||
onClick={this.save}
|
||||
text={
|
||||
this.state.existing
|
||||
? intl.get("edit")
|
||||
: intl.get("confirm")
|
||||
}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
{this.state.existing ? (
|
||||
<DangerButton
|
||||
onClick={this.remove}
|
||||
text={intl.get("delete")}
|
||||
/>
|
||||
) : (
|
||||
<DefaultButton
|
||||
onClick={this.props.exit}
|
||||
text={intl.get("cancel")}
|
||||
/>
|
||||
)}
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
{this.state.existing && (
|
||||
<LiteExporter serviceConfigs={this.props.configs} />
|
||||
)}
|
||||
</Stack>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>Email</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
disabled={this.state.existing}
|
||||
onGetErrorMessage={this.checkNotEmpty}
|
||||
validateOnLoad={false}
|
||||
name="username"
|
||||
value={this.state.username}
|
||||
onChange={this.handleInputChange} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("service.password")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
type="password"
|
||||
placeholder={this.state.existing ? intl.get("service.unchanged") : ""}
|
||||
onGetErrorMessage={this.checkNotEmpty}
|
||||
validateOnLoad={false}
|
||||
name="password"
|
||||
value={this.state.password}
|
||||
onChange={this.handleInputChange} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("service.fetchLimit")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<Dropdown
|
||||
options={this.fetchLimitOptions()}
|
||||
selectedKey={this.state.fetchLimit}
|
||||
onChange={this.onFetchLimitOptionChange} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
{!this.state.existing && <Checkbox
|
||||
label={intl.get("service.importGroups")}
|
||||
checked={this.state.importGroups}
|
||||
onChange={(_, c) => this.setState({importGroups: c})} />}
|
||||
<Stack horizontal style={{marginTop: 32}}>
|
||||
<Stack.Item>
|
||||
<PrimaryButton
|
||||
disabled={!this.validateForm()}
|
||||
onClick={this.save}
|
||||
text={this.state.existing ? intl.get("edit") : intl.get("confirm")} />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
{this.state.existing
|
||||
? <DangerButton onClick={this.remove} text={intl.get("delete")} />
|
||||
: <DefaultButton onClick={this.props.exit} text={intl.get("cancel")} />
|
||||
}
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
{ this.state.existing && <LiteExporter serviceConfigs={this.props.configs} /> }
|
||||
</Stack>
|
||||
</>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default FeedbinConfigsTab
|
||||
export default FeedbinConfigsTab
|
||||
|
|
|
@ -4,7 +4,19 @@ import md5 from "js-md5"
|
|||
import { ServiceConfigsTabProps } from "../service"
|
||||
import { FeverConfigs } from "../../../scripts/models/services/fever"
|
||||
import { SyncService } from "../../../schema-types"
|
||||
import { Stack, Icon, Label, TextField, PrimaryButton, DefaultButton, Checkbox, MessageBar, MessageBarType, Dropdown, IDropdownOption } from "@fluentui/react"
|
||||
import {
|
||||
Stack,
|
||||
Icon,
|
||||
Label,
|
||||
TextField,
|
||||
PrimaryButton,
|
||||
DefaultButton,
|
||||
Checkbox,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
Dropdown,
|
||||
IDropdownOption,
|
||||
} from "@fluentui/react"
|
||||
import DangerButton from "../../utils/danger-button"
|
||||
import { urlTest } from "../../../scripts/utils"
|
||||
import LiteExporter from "./lite-exporter"
|
||||
|
@ -18,7 +30,10 @@ type FeverConfigsTabState = {
|
|||
importGroups: boolean
|
||||
}
|
||||
|
||||
class FeverConfigsTab extends React.Component<ServiceConfigsTabProps, FeverConfigsTabState> {
|
||||
class FeverConfigsTab extends React.Component<
|
||||
ServiceConfigsTabProps,
|
||||
FeverConfigsTabState
|
||||
> {
|
||||
constructor(props: ServiceConfigsTabProps) {
|
||||
super(props)
|
||||
const configs = props.configs as FeverConfigs
|
||||
|
@ -38,24 +53,33 @@ class FeverConfigsTab extends React.Component<ServiceConfigsTabProps, FeverConfi
|
|||
{ key: 750, text: intl.get("service.fetchLimitNum", { count: 750 }) },
|
||||
{ key: 1000, text: intl.get("service.fetchLimitNum", { count: 1000 }) },
|
||||
{ key: 1500, text: intl.get("service.fetchLimitNum", { count: 1500 }) },
|
||||
{ key: Number.MAX_SAFE_INTEGER, text: intl.get("service.fetchUnlimited") },
|
||||
{
|
||||
key: Number.MAX_SAFE_INTEGER,
|
||||
text: intl.get("service.fetchUnlimited"),
|
||||
},
|
||||
]
|
||||
onFetchLimitOptionChange = (_, option: IDropdownOption) => {
|
||||
this.setState({ fetchLimit: option.key as number })
|
||||
}
|
||||
|
||||
handleInputChange = (event) => {
|
||||
handleInputChange = event => {
|
||||
const name: string = event.target.name
|
||||
// @ts-expect-error
|
||||
this.setState({[name]: event.target.value})
|
||||
this.setState({ [name]: event.target.value })
|
||||
}
|
||||
|
||||
checkNotEmpty = (v: string) => {
|
||||
return (!this.state.existing && v.length == 0) ? intl.get("emptyField") : ""
|
||||
return !this.state.existing && v.length == 0
|
||||
? intl.get("emptyField")
|
||||
: ""
|
||||
}
|
||||
|
||||
validateForm = () => {
|
||||
return urlTest(this.state.endpoint.trim()) && (this.state.existing || (this.state.username && this.state.password))
|
||||
return (
|
||||
urlTest(this.state.endpoint.trim()) &&
|
||||
(this.state.existing ||
|
||||
(this.state.username && this.state.password))
|
||||
)
|
||||
}
|
||||
|
||||
save = async () => {
|
||||
|
@ -64,17 +88,19 @@ class FeverConfigsTab extends React.Component<ServiceConfigsTabProps, FeverConfi
|
|||
configs = {
|
||||
...this.props.configs,
|
||||
endpoint: this.state.endpoint,
|
||||
fetchLimit: this.state.fetchLimit
|
||||
fetchLimit: this.state.fetchLimit,
|
||||
} as FeverConfigs
|
||||
if (this.state.password)
|
||||
configs.apiKey = md5(`${configs.username}:${this.state.password}`)
|
||||
if (this.state.password)
|
||||
configs.apiKey = md5(
|
||||
`${configs.username}:${this.state.password}`
|
||||
)
|
||||
} else {
|
||||
configs = {
|
||||
type: SyncService.Fever,
|
||||
endpoint: this.state.endpoint,
|
||||
username: this.state.username,
|
||||
fetchLimit: this.state.fetchLimit,
|
||||
apiKey: md5(`${this.state.username}:${this.state.password}`)
|
||||
apiKey: md5(`${this.state.username}:${this.state.password}`),
|
||||
}
|
||||
if (this.state.importGroups) configs.importGroups = true
|
||||
}
|
||||
|
@ -86,7 +112,10 @@ class FeverConfigsTab extends React.Component<ServiceConfigsTabProps, FeverConfi
|
|||
this.props.sync()
|
||||
} else {
|
||||
this.props.blockActions()
|
||||
window.utils.showErrorBox(intl.get("service.failure"), intl.get("service.failureHint"))
|
||||
window.utils.showErrorBox(
|
||||
intl.get("service.failure"),
|
||||
intl.get("service.failureHint")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,88 +125,130 @@ class FeverConfigsTab extends React.Component<ServiceConfigsTabProps, FeverConfi
|
|||
}
|
||||
|
||||
render() {
|
||||
return <>
|
||||
{!this.state.existing && (
|
||||
<MessageBar messageBarType={MessageBarType.warning}>{intl.get("service.overwriteWarning")}</MessageBar>
|
||||
)}
|
||||
<Stack horizontalAlign="center" style={{marginTop: 48}}>
|
||||
<Icon iconName="Calories" style={{color: "var(--black)", fontSize: 32, userSelect: "none"}} />
|
||||
<Label style={{margin: "8px 0 36px"}}>Fever API</Label>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("service.endpoint")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
onGetErrorMessage={v => urlTest(v.trim()) ? "" : intl.get("sources.badUrl")}
|
||||
validateOnLoad={false}
|
||||
name="endpoint"
|
||||
value={this.state.endpoint}
|
||||
onChange={this.handleInputChange} />
|
||||
</Stack.Item>
|
||||
return (
|
||||
<>
|
||||
{!this.state.existing && (
|
||||
<MessageBar messageBarType={MessageBarType.warning}>
|
||||
{intl.get("service.overwriteWarning")}
|
||||
</MessageBar>
|
||||
)}
|
||||
<Stack horizontalAlign="center" style={{ marginTop: 48 }}>
|
||||
<Icon
|
||||
iconName="Calories"
|
||||
style={{
|
||||
color: "var(--black)",
|
||||
fontSize: 32,
|
||||
userSelect: "none",
|
||||
}}
|
||||
/>
|
||||
<Label style={{ margin: "8px 0 36px" }}>Fever API</Label>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("service.endpoint")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
onGetErrorMessage={v =>
|
||||
urlTest(v.trim())
|
||||
? ""
|
||||
: intl.get("sources.badUrl")
|
||||
}
|
||||
validateOnLoad={false}
|
||||
name="endpoint"
|
||||
value={this.state.endpoint}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("service.username")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
disabled={this.state.existing}
|
||||
onGetErrorMessage={this.checkNotEmpty}
|
||||
validateOnLoad={false}
|
||||
name="username"
|
||||
value={this.state.username}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("service.password")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
type="password"
|
||||
placeholder={
|
||||
this.state.existing
|
||||
? intl.get("service.unchanged")
|
||||
: ""
|
||||
}
|
||||
onGetErrorMessage={this.checkNotEmpty}
|
||||
validateOnLoad={false}
|
||||
name="password"
|
||||
value={this.state.password}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("service.fetchLimit")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<Dropdown
|
||||
options={this.fetchLimitOptions()}
|
||||
selectedKey={this.state.fetchLimit}
|
||||
onChange={this.onFetchLimitOptionChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
{!this.state.existing && (
|
||||
<Checkbox
|
||||
label={intl.get("service.importGroups")}
|
||||
checked={this.state.importGroups}
|
||||
onChange={(_, c) =>
|
||||
this.setState({ importGroups: c })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Stack horizontal style={{ marginTop: 32 }}>
|
||||
<Stack.Item>
|
||||
<PrimaryButton
|
||||
disabled={!this.validateForm()}
|
||||
onClick={this.save}
|
||||
text={
|
||||
this.state.existing
|
||||
? intl.get("edit")
|
||||
: intl.get("confirm")
|
||||
}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
{this.state.existing ? (
|
||||
<DangerButton
|
||||
onClick={this.remove}
|
||||
text={intl.get("delete")}
|
||||
/>
|
||||
) : (
|
||||
<DefaultButton
|
||||
onClick={this.props.exit}
|
||||
text={intl.get("cancel")}
|
||||
/>
|
||||
)}
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
{this.state.existing && (
|
||||
<LiteExporter serviceConfigs={this.props.configs} />
|
||||
)}
|
||||
</Stack>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("service.username")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
disabled={this.state.existing}
|
||||
onGetErrorMessage={this.checkNotEmpty}
|
||||
validateOnLoad={false}
|
||||
name="username"
|
||||
value={this.state.username}
|
||||
onChange={this.handleInputChange} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("service.password")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
type="password"
|
||||
placeholder={this.state.existing ? intl.get("service.unchanged") : ""}
|
||||
onGetErrorMessage={this.checkNotEmpty}
|
||||
validateOnLoad={false}
|
||||
name="password"
|
||||
value={this.state.password}
|
||||
onChange={this.handleInputChange} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("service.fetchLimit")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<Dropdown
|
||||
options={this.fetchLimitOptions()}
|
||||
selectedKey={this.state.fetchLimit}
|
||||
onChange={this.onFetchLimitOptionChange} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
{!this.state.existing && <Checkbox
|
||||
label={intl.get("service.importGroups")}
|
||||
checked={this.state.importGroups}
|
||||
onChange={(_, c) => this.setState({importGroups: c})} />}
|
||||
<Stack horizontal style={{marginTop: 32}}>
|
||||
<Stack.Item>
|
||||
<PrimaryButton
|
||||
disabled={!this.validateForm()}
|
||||
onClick={this.save}
|
||||
text={this.state.existing ? intl.get("edit") : intl.get("confirm")} />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
{this.state.existing
|
||||
? <DangerButton onClick={this.remove} text={intl.get("delete")} />
|
||||
: <DefaultButton onClick={this.props.exit} text={intl.get("cancel")} />
|
||||
}
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
{ this.state.existing && <LiteExporter serviceConfigs={this.props.configs} /> }
|
||||
</Stack>
|
||||
</>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default FeverConfigsTab
|
||||
export default FeverConfigsTab
|
||||
|
|
|
@ -3,7 +3,19 @@ import intl from "react-intl-universal"
|
|||
import { ServiceConfigsTabProps } from "../service"
|
||||
import { GReaderConfigs } from "../../../scripts/models/services/greader"
|
||||
import { SyncService } from "../../../schema-types"
|
||||
import { Stack, Icon, Label, TextField, PrimaryButton, DefaultButton, Checkbox, MessageBar, MessageBarType, Dropdown, IDropdownOption } from "@fluentui/react"
|
||||
import {
|
||||
Stack,
|
||||
Icon,
|
||||
Label,
|
||||
TextField,
|
||||
PrimaryButton,
|
||||
DefaultButton,
|
||||
Checkbox,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
Dropdown,
|
||||
IDropdownOption,
|
||||
} from "@fluentui/react"
|
||||
import DangerButton from "../../utils/danger-button"
|
||||
import { urlTest } from "../../../scripts/utils"
|
||||
import LiteExporter from "./lite-exporter"
|
||||
|
@ -17,7 +29,10 @@ type GReaderConfigsTabState = {
|
|||
importGroups: boolean
|
||||
}
|
||||
|
||||
class GReaderConfigsTab extends React.Component<ServiceConfigsTabProps, GReaderConfigsTabState> {
|
||||
class GReaderConfigsTab extends React.Component<
|
||||
ServiceConfigsTabProps,
|
||||
GReaderConfigsTabState
|
||||
> {
|
||||
constructor(props: ServiceConfigsTabProps) {
|
||||
super(props)
|
||||
const configs = props.configs as GReaderConfigs
|
||||
|
@ -37,24 +52,33 @@ class GReaderConfigsTab extends React.Component<ServiceConfigsTabProps, GReaderC
|
|||
{ key: 750, text: intl.get("service.fetchLimitNum", { count: 750 }) },
|
||||
{ key: 1000, text: intl.get("service.fetchLimitNum", { count: 1000 }) },
|
||||
{ key: 1500, text: intl.get("service.fetchLimitNum", { count: 1500 }) },
|
||||
{ key: Number.MAX_SAFE_INTEGER, text: intl.get("service.fetchUnlimited") },
|
||||
{
|
||||
key: Number.MAX_SAFE_INTEGER,
|
||||
text: intl.get("service.fetchUnlimited"),
|
||||
},
|
||||
]
|
||||
onFetchLimitOptionChange = (_, option: IDropdownOption) => {
|
||||
this.setState({ fetchLimit: option.key as number })
|
||||
}
|
||||
|
||||
handleInputChange = (event) => {
|
||||
handleInputChange = event => {
|
||||
const name: string = event.target.name
|
||||
// @ts-expect-error
|
||||
this.setState({[name]: event.target.value})
|
||||
this.setState({ [name]: event.target.value })
|
||||
}
|
||||
|
||||
checkNotEmpty = (v: string) => {
|
||||
return (!this.state.existing && v.length == 0) ? intl.get("emptyField") : ""
|
||||
return !this.state.existing && v.length == 0
|
||||
? intl.get("emptyField")
|
||||
: ""
|
||||
}
|
||||
|
||||
validateForm = () => {
|
||||
return urlTest(this.state.endpoint.trim()) && (this.state.existing || (this.state.username && this.state.password))
|
||||
return (
|
||||
urlTest(this.state.endpoint.trim()) &&
|
||||
(this.state.existing ||
|
||||
(this.state.username && this.state.password))
|
||||
)
|
||||
}
|
||||
|
||||
save = async () => {
|
||||
|
@ -63,7 +87,7 @@ class GReaderConfigsTab extends React.Component<ServiceConfigsTabProps, GReaderC
|
|||
configs = {
|
||||
...this.props.configs,
|
||||
endpoint: this.state.endpoint,
|
||||
fetchLimit: this.state.fetchLimit
|
||||
fetchLimit: this.state.fetchLimit,
|
||||
} as GReaderConfigs
|
||||
if (this.state.password) configs.password = this.state.password
|
||||
} else {
|
||||
|
@ -73,12 +97,12 @@ class GReaderConfigsTab extends React.Component<ServiceConfigsTabProps, GReaderC
|
|||
username: this.state.username,
|
||||
password: this.state.password,
|
||||
fetchLimit: this.state.fetchLimit,
|
||||
useInt64: !this.state.endpoint.endsWith("theoldreader.com")
|
||||
useInt64: !this.state.endpoint.endsWith("theoldreader.com"),
|
||||
}
|
||||
if (this.state.importGroups) configs.importGroups = true
|
||||
}
|
||||
this.props.blockActions()
|
||||
configs = await this.props.reauthenticate(configs) as GReaderConfigs
|
||||
configs = (await this.props.reauthenticate(configs)) as GReaderConfigs
|
||||
const valid = await this.props.authenticate(configs)
|
||||
if (valid) {
|
||||
this.props.save(configs)
|
||||
|
@ -86,7 +110,10 @@ class GReaderConfigsTab extends React.Component<ServiceConfigsTabProps, GReaderC
|
|||
this.props.sync()
|
||||
} else {
|
||||
this.props.blockActions()
|
||||
window.utils.showErrorBox(intl.get("service.failure"), intl.get("service.failureHint"))
|
||||
window.utils.showErrorBox(
|
||||
intl.get("service.failure"),
|
||||
intl.get("service.failureHint")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,88 +123,133 @@ class GReaderConfigsTab extends React.Component<ServiceConfigsTabProps, GReaderC
|
|||
}
|
||||
|
||||
render() {
|
||||
return <>
|
||||
{!this.state.existing && (
|
||||
<MessageBar messageBarType={MessageBarType.warning}>{intl.get("service.overwriteWarning")}</MessageBar>
|
||||
)}
|
||||
<Stack horizontalAlign="center" style={{marginTop: 48}}>
|
||||
<Icon iconName="Communications" style={{color: "var(--black)", transform: "rotate(220deg)", fontSize: 32, userSelect: "none"}} />
|
||||
<Label style={{margin: "8px 0 36px"}}>Google Reader API</Label>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("service.endpoint")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
onGetErrorMessage={v => urlTest(v.trim()) ? "" : intl.get("sources.badUrl")}
|
||||
validateOnLoad={false}
|
||||
name="endpoint"
|
||||
value={this.state.endpoint}
|
||||
onChange={this.handleInputChange} />
|
||||
</Stack.Item>
|
||||
return (
|
||||
<>
|
||||
{!this.state.existing && (
|
||||
<MessageBar messageBarType={MessageBarType.warning}>
|
||||
{intl.get("service.overwriteWarning")}
|
||||
</MessageBar>
|
||||
)}
|
||||
<Stack horizontalAlign="center" style={{ marginTop: 48 }}>
|
||||
<Icon
|
||||
iconName="Communications"
|
||||
style={{
|
||||
color: "var(--black)",
|
||||
transform: "rotate(220deg)",
|
||||
fontSize: 32,
|
||||
userSelect: "none",
|
||||
}}
|
||||
/>
|
||||
<Label style={{ margin: "8px 0 36px" }}>
|
||||
Google Reader API
|
||||
</Label>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("service.endpoint")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
onGetErrorMessage={v =>
|
||||
urlTest(v.trim())
|
||||
? ""
|
||||
: intl.get("sources.badUrl")
|
||||
}
|
||||
validateOnLoad={false}
|
||||
name="endpoint"
|
||||
value={this.state.endpoint}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("service.username")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
disabled={this.state.existing}
|
||||
onGetErrorMessage={this.checkNotEmpty}
|
||||
validateOnLoad={false}
|
||||
name="username"
|
||||
value={this.state.username}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("service.password")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
type="password"
|
||||
placeholder={
|
||||
this.state.existing
|
||||
? intl.get("service.unchanged")
|
||||
: ""
|
||||
}
|
||||
onGetErrorMessage={this.checkNotEmpty}
|
||||
validateOnLoad={false}
|
||||
name="password"
|
||||
value={this.state.password}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("service.fetchLimit")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<Dropdown
|
||||
options={this.fetchLimitOptions()}
|
||||
selectedKey={this.state.fetchLimit}
|
||||
onChange={this.onFetchLimitOptionChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
{!this.state.existing && (
|
||||
<Checkbox
|
||||
label={intl.get("service.importGroups")}
|
||||
checked={this.state.importGroups}
|
||||
onChange={(_, c) =>
|
||||
this.setState({ importGroups: c })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Stack horizontal style={{ marginTop: 32 }}>
|
||||
<Stack.Item>
|
||||
<PrimaryButton
|
||||
disabled={!this.validateForm()}
|
||||
onClick={this.save}
|
||||
text={
|
||||
this.state.existing
|
||||
? intl.get("edit")
|
||||
: intl.get("confirm")
|
||||
}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
{this.state.existing ? (
|
||||
<DangerButton
|
||||
onClick={this.remove}
|
||||
text={intl.get("delete")}
|
||||
/>
|
||||
) : (
|
||||
<DefaultButton
|
||||
onClick={this.props.exit}
|
||||
text={intl.get("cancel")}
|
||||
/>
|
||||
)}
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
{this.state.existing && (
|
||||
<LiteExporter serviceConfigs={this.props.configs} />
|
||||
)}
|
||||
</Stack>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("service.username")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
disabled={this.state.existing}
|
||||
onGetErrorMessage={this.checkNotEmpty}
|
||||
validateOnLoad={false}
|
||||
name="username"
|
||||
value={this.state.username}
|
||||
onChange={this.handleInputChange} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("service.password")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
type="password"
|
||||
placeholder={this.state.existing ? intl.get("service.unchanged") : ""}
|
||||
onGetErrorMessage={this.checkNotEmpty}
|
||||
validateOnLoad={false}
|
||||
name="password"
|
||||
value={this.state.password}
|
||||
onChange={this.handleInputChange} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("service.fetchLimit")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<Dropdown
|
||||
options={this.fetchLimitOptions()}
|
||||
selectedKey={this.state.fetchLimit}
|
||||
onChange={this.onFetchLimitOptionChange} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
{!this.state.existing && <Checkbox
|
||||
label={intl.get("service.importGroups")}
|
||||
checked={this.state.importGroups}
|
||||
onChange={(_, c) => this.setState({importGroups: c})} />}
|
||||
<Stack horizontal style={{marginTop: 32}}>
|
||||
<Stack.Item>
|
||||
<PrimaryButton
|
||||
disabled={!this.validateForm()}
|
||||
onClick={this.save}
|
||||
text={this.state.existing ? intl.get("edit") : intl.get("confirm")} />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
{this.state.existing
|
||||
? <DangerButton onClick={this.remove} text={intl.get("delete")} />
|
||||
: <DefaultButton onClick={this.props.exit} text={intl.get("cancel")} />
|
||||
}
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
{ this.state.existing && <LiteExporter serviceConfigs={this.props.configs} /> }
|
||||
</Stack>
|
||||
</>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default GReaderConfigsTab
|
||||
export default GReaderConfigsTab
|
||||
|
|
|
@ -3,8 +3,20 @@ import intl from "react-intl-universal"
|
|||
import { ServiceConfigsTabProps } from "../service"
|
||||
import { GReaderConfigs } from "../../../scripts/models/services/greader"
|
||||
import { SyncService } from "../../../schema-types"
|
||||
import { Stack, Label, TextField, PrimaryButton, DefaultButton, Checkbox,
|
||||
MessageBar, MessageBarType, Dropdown, IDropdownOption, MessageBarButton, Link } from "@fluentui/react"
|
||||
import {
|
||||
Stack,
|
||||
Label,
|
||||
TextField,
|
||||
PrimaryButton,
|
||||
DefaultButton,
|
||||
Checkbox,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
Dropdown,
|
||||
IDropdownOption,
|
||||
MessageBarButton,
|
||||
Link,
|
||||
} from "@fluentui/react"
|
||||
import DangerButton from "../../utils/danger-button"
|
||||
import LiteExporter from "./lite-exporter"
|
||||
|
||||
|
@ -22,12 +34,18 @@ type GReaderConfigsTabState = {
|
|||
const endpointOptions: IDropdownOption[] = [
|
||||
"https://www.inoreader.com",
|
||||
"https://www.innoreader.com",
|
||||
"https://jp.inoreader.com"
|
||||
"https://jp.inoreader.com",
|
||||
].map(s => ({ key: s, text: s }))
|
||||
|
||||
const openSupport = () => window.utils.openExternal("https://github.com/yang991178/fluent-reader/wiki/Support#inoreader")
|
||||
const openSupport = () =>
|
||||
window.utils.openExternal(
|
||||
"https://github.com/yang991178/fluent-reader/wiki/Support#inoreader"
|
||||
)
|
||||
|
||||
class InoreaderConfigsTab extends React.Component<ServiceConfigsTabProps, GReaderConfigsTabState> {
|
||||
class InoreaderConfigsTab extends React.Component<
|
||||
ServiceConfigsTabProps,
|
||||
GReaderConfigsTabState
|
||||
> {
|
||||
constructor(props: ServiceConfigsTabProps) {
|
||||
super(props)
|
||||
const configs = props.configs as GReaderConfigs
|
||||
|
@ -38,7 +56,10 @@ class InoreaderConfigsTab extends React.Component<ServiceConfigsTabProps, GReade
|
|||
password: "",
|
||||
apiId: configs.inoreaderId || "",
|
||||
apiKey: configs.inoreaderKey || "",
|
||||
removeAd: configs.removeInoreaderAd === undefined ? true : configs.removeInoreaderAd,
|
||||
removeAd:
|
||||
configs.removeInoreaderAd === undefined
|
||||
? true
|
||||
: configs.removeInoreaderAd,
|
||||
fetchLimit: configs.fetchLimit || 250,
|
||||
}
|
||||
}
|
||||
|
@ -48,7 +69,10 @@ class InoreaderConfigsTab extends React.Component<ServiceConfigsTabProps, GReade
|
|||
{ key: 500, text: intl.get("service.fetchLimitNum", { count: 500 }) },
|
||||
{ key: 750, text: intl.get("service.fetchLimitNum", { count: 750 }) },
|
||||
{ key: 1000, text: intl.get("service.fetchLimitNum", { count: 1000 }) },
|
||||
{ key: Number.MAX_SAFE_INTEGER, text: intl.get("service.fetchUnlimited") },
|
||||
{
|
||||
key: Number.MAX_SAFE_INTEGER,
|
||||
text: intl.get("service.fetchUnlimited"),
|
||||
},
|
||||
]
|
||||
onFetchLimitOptionChange = (_, option: IDropdownOption) => {
|
||||
this.setState({ fetchLimit: option.key as number })
|
||||
|
@ -57,19 +81,25 @@ class InoreaderConfigsTab extends React.Component<ServiceConfigsTabProps, GReade
|
|||
this.setState({ endpoint: option.key as string })
|
||||
}
|
||||
|
||||
handleInputChange = (event) => {
|
||||
handleInputChange = event => {
|
||||
const name: string = event.target.name
|
||||
// @ts-expect-error
|
||||
this.setState({[name]: event.target.value})
|
||||
this.setState({ [name]: event.target.value })
|
||||
}
|
||||
|
||||
checkNotEmpty = (v: string) => {
|
||||
return (!this.state.existing && v.length == 0) ? intl.get("emptyField") : ""
|
||||
return !this.state.existing && v.length == 0
|
||||
? intl.get("emptyField")
|
||||
: ""
|
||||
}
|
||||
|
||||
validateForm = () => {
|
||||
return (this.state.existing || (this.state.username && this.state.password))
|
||||
&& this.state.apiId && this.state.apiKey
|
||||
return (
|
||||
(this.state.existing ||
|
||||
(this.state.username && this.state.password)) &&
|
||||
this.state.apiId &&
|
||||
this.state.apiKey
|
||||
)
|
||||
}
|
||||
|
||||
save = async () => {
|
||||
|
@ -95,11 +125,11 @@ class InoreaderConfigsTab extends React.Component<ServiceConfigsTabProps, GReade
|
|||
removeInoreaderAd: this.state.removeAd,
|
||||
fetchLimit: this.state.fetchLimit,
|
||||
importGroups: true,
|
||||
useInt64: true
|
||||
useInt64: true,
|
||||
}
|
||||
}
|
||||
this.props.blockActions()
|
||||
configs = await this.props.reauthenticate(configs) as GReaderConfigs
|
||||
configs = (await this.props.reauthenticate(configs)) as GReaderConfigs
|
||||
const valid = await this.props.authenticate(configs)
|
||||
if (valid) {
|
||||
this.props.save(configs)
|
||||
|
@ -107,11 +137,17 @@ class InoreaderConfigsTab extends React.Component<ServiceConfigsTabProps, GReade
|
|||
this.props.sync()
|
||||
} else {
|
||||
this.props.blockActions()
|
||||
window.utils.showErrorBox(intl.get("service.failure"), intl.get("service.failureHint"))
|
||||
window.utils.showErrorBox(
|
||||
intl.get("service.failure"),
|
||||
intl.get("service.failureHint")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
createKey = () => window.utils.openExternal(this.state.endpoint + "/all_articles#preferences-developer")
|
||||
createKey = () =>
|
||||
window.utils.openExternal(
|
||||
this.state.endpoint + "/all_articles#preferences-developer"
|
||||
)
|
||||
|
||||
remove = async () => {
|
||||
this.props.exit()
|
||||
|
@ -119,118 +155,165 @@ class InoreaderConfigsTab extends React.Component<ServiceConfigsTabProps, GReade
|
|||
}
|
||||
|
||||
render() {
|
||||
return <>
|
||||
<MessageBar messageBarType={MessageBarType.severeWarning}
|
||||
isMultiline={false}
|
||||
actions={<MessageBarButton text={intl.get("create")} onClick={this.createKey} />}>
|
||||
{intl.get("service.rateLimitWarning")}
|
||||
<Link onClick={openSupport} style={{marginLeft: 6}}>{intl.get("rules.help")}</Link>
|
||||
</MessageBar>
|
||||
{!this.state.existing && (
|
||||
<MessageBar messageBarType={MessageBarType.warning}>{intl.get("service.overwriteWarning")}</MessageBar>
|
||||
)}
|
||||
<Stack horizontalAlign="center" style={{marginTop: 48}}>
|
||||
<svg style={{fill: "var(--black)", width: 36, userSelect: "none"}} viewBox="0 0 72 72"><path transform="translate(-1250.000000, -1834.000000)" d="M1286,1834 C1305.88225,1834 1322,1850.11775 1322,1870 C1322,1889.88225 1305.88225,1906 1286,1906 C1266.11775,1906 1250,1889.88225 1250,1870 C1250,1850.11775 1266.11775,1834 1286,1834 Z M1278.01029,1864.98015 C1270.82534,1864.98015 1265,1870.80399 1265,1877.98875 C1265,1885.17483 1270.82534,1891 1278.01029,1891 C1285.19326,1891 1291.01859,1885.17483 1291.01859,1877.98875 C1291.01859,1870.80399 1285.19326,1864.98015 1278.01029,1864.98015 Z M1281.67908,1870.54455 C1283.73609,1870.54455 1285.40427,1872.21533 1285.40427,1874.2703 C1285.40427,1876.33124 1283.73609,1877.9987 1281.67908,1877.9987 C1279.61941,1877.9987 1277.94991,1876.33124 1277.94991,1874.2703 C1277.94991,1872.21533 1279.61941,1870.54455 1281.67908,1870.54455 Z M1278.01003,1855.78714 L1278.01003,1860.47435 C1287.66605,1860.47435 1295.52584,1868.33193 1295.52584,1877.98901 L1295.52584,1877.98901 L1300.21451,1877.98901 C1300.21451,1865.74746 1290.25391,1855.78714 1278.01003,1855.78714 L1278.01003,1855.78714 Z M1278.01009,1846 L1278.01009,1850.68721 C1285.30188,1850.68721 1292.15771,1853.5278 1297.31618,1858.68479 C1302.47398,1863.84179 1305.31067,1870.69942 1305.31067,1877.98901 L1305.31067,1877.98901 L1310,1877.98901 C1310,1869.44534 1306.67162,1861.41192 1300.6293,1855.36845 C1294.58632,1849.32696 1286.55533,1846 1278.01009,1846 L1278.01009,1846 Z"></path></svg>
|
||||
<Label style={{margin: "8px 0 36px"}}>Inoreader</Label>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("service.endpoint")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<Dropdown
|
||||
options={endpointOptions}
|
||||
selectedKey={this.state.endpoint}
|
||||
onChange={this.onEndpointChange} />
|
||||
</Stack.Item>
|
||||
return (
|
||||
<>
|
||||
<MessageBar
|
||||
messageBarType={MessageBarType.severeWarning}
|
||||
isMultiline={false}
|
||||
actions={
|
||||
<MessageBarButton
|
||||
text={intl.get("create")}
|
||||
onClick={this.createKey}
|
||||
/>
|
||||
}>
|
||||
{intl.get("service.rateLimitWarning")}
|
||||
<Link onClick={openSupport} style={{ marginLeft: 6 }}>
|
||||
{intl.get("rules.help")}
|
||||
</Link>
|
||||
</MessageBar>
|
||||
{!this.state.existing && (
|
||||
<MessageBar messageBarType={MessageBarType.warning}>
|
||||
{intl.get("service.overwriteWarning")}
|
||||
</MessageBar>
|
||||
)}
|
||||
<Stack horizontalAlign="center" style={{ marginTop: 48 }}>
|
||||
<svg
|
||||
style={{
|
||||
fill: "var(--black)",
|
||||
width: 36,
|
||||
userSelect: "none",
|
||||
}}
|
||||
viewBox="0 0 72 72">
|
||||
<path
|
||||
transform="translate(-1250.000000, -1834.000000)"
|
||||
d="M1286,1834 C1305.88225,1834 1322,1850.11775 1322,1870 C1322,1889.88225 1305.88225,1906 1286,1906 C1266.11775,1906 1250,1889.88225 1250,1870 C1250,1850.11775 1266.11775,1834 1286,1834 Z M1278.01029,1864.98015 C1270.82534,1864.98015 1265,1870.80399 1265,1877.98875 C1265,1885.17483 1270.82534,1891 1278.01029,1891 C1285.19326,1891 1291.01859,1885.17483 1291.01859,1877.98875 C1291.01859,1870.80399 1285.19326,1864.98015 1278.01029,1864.98015 Z M1281.67908,1870.54455 C1283.73609,1870.54455 1285.40427,1872.21533 1285.40427,1874.2703 C1285.40427,1876.33124 1283.73609,1877.9987 1281.67908,1877.9987 C1279.61941,1877.9987 1277.94991,1876.33124 1277.94991,1874.2703 C1277.94991,1872.21533 1279.61941,1870.54455 1281.67908,1870.54455 Z M1278.01003,1855.78714 L1278.01003,1860.47435 C1287.66605,1860.47435 1295.52584,1868.33193 1295.52584,1877.98901 L1295.52584,1877.98901 L1300.21451,1877.98901 C1300.21451,1865.74746 1290.25391,1855.78714 1278.01003,1855.78714 L1278.01003,1855.78714 Z M1278.01009,1846 L1278.01009,1850.68721 C1285.30188,1850.68721 1292.15771,1853.5278 1297.31618,1858.68479 C1302.47398,1863.84179 1305.31067,1870.69942 1305.31067,1877.98901 L1305.31067,1877.98901 L1310,1877.98901 C1310,1869.44534 1306.67162,1861.41192 1300.6293,1855.36845 C1294.58632,1849.32696 1286.55533,1846 1278.01009,1846 L1278.01009,1846 Z"></path>
|
||||
</svg>
|
||||
<Label style={{ margin: "8px 0 36px" }}>Inoreader</Label>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("service.endpoint")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<Dropdown
|
||||
options={endpointOptions}
|
||||
selectedKey={this.state.endpoint}
|
||||
onChange={this.onEndpointChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("service.username")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
disabled={this.state.existing}
|
||||
onGetErrorMessage={this.checkNotEmpty}
|
||||
validateOnLoad={false}
|
||||
name="username"
|
||||
value={this.state.username}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("service.password")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
type="password"
|
||||
placeholder={
|
||||
this.state.existing
|
||||
? intl.get("service.unchanged")
|
||||
: ""
|
||||
}
|
||||
onGetErrorMessage={this.checkNotEmpty}
|
||||
validateOnLoad={false}
|
||||
name="password"
|
||||
value={this.state.password}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>API ID</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
onGetErrorMessage={this.checkNotEmpty}
|
||||
validateOnLoad={false}
|
||||
name="apiId"
|
||||
value={this.state.apiId}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>API Key</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
onGetErrorMessage={this.checkNotEmpty}
|
||||
validateOnLoad={false}
|
||||
name="apiKey"
|
||||
value={this.state.apiKey}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("service.fetchLimit")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<Dropdown
|
||||
options={this.fetchLimitOptions()}
|
||||
selectedKey={this.state.fetchLimit}
|
||||
onChange={this.onFetchLimitOptionChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Checkbox
|
||||
label={intl.get("service.removeAd")}
|
||||
checked={this.state.removeAd}
|
||||
onChange={(_, c) => this.setState({ removeAd: c })}
|
||||
/>
|
||||
<Stack horizontal style={{ marginTop: 32 }}>
|
||||
<Stack.Item>
|
||||
<PrimaryButton
|
||||
disabled={!this.validateForm()}
|
||||
onClick={this.save}
|
||||
text={
|
||||
this.state.existing
|
||||
? intl.get("edit")
|
||||
: intl.get("confirm")
|
||||
}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
{this.state.existing ? (
|
||||
<DangerButton
|
||||
onClick={this.remove}
|
||||
text={intl.get("delete")}
|
||||
/>
|
||||
) : (
|
||||
<DefaultButton
|
||||
onClick={this.props.exit}
|
||||
text={intl.get("cancel")}
|
||||
/>
|
||||
)}
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
{this.state.existing && (
|
||||
<LiteExporter serviceConfigs={this.props.configs} />
|
||||
)}
|
||||
</Stack>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("service.username")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
disabled={this.state.existing}
|
||||
onGetErrorMessage={this.checkNotEmpty}
|
||||
validateOnLoad={false}
|
||||
name="username"
|
||||
value={this.state.username}
|
||||
onChange={this.handleInputChange} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("service.password")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
type="password"
|
||||
placeholder={this.state.existing ? intl.get("service.unchanged") : ""}
|
||||
onGetErrorMessage={this.checkNotEmpty}
|
||||
validateOnLoad={false}
|
||||
name="password"
|
||||
value={this.state.password}
|
||||
onChange={this.handleInputChange} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>API ID</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
onGetErrorMessage={this.checkNotEmpty}
|
||||
validateOnLoad={false}
|
||||
name="apiId"
|
||||
value={this.state.apiId}
|
||||
onChange={this.handleInputChange} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>API Key</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
onGetErrorMessage={this.checkNotEmpty}
|
||||
validateOnLoad={false}
|
||||
name="apiKey"
|
||||
value={this.state.apiKey}
|
||||
onChange={this.handleInputChange} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("service.fetchLimit")}</Label>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<Dropdown
|
||||
options={this.fetchLimitOptions()}
|
||||
selectedKey={this.state.fetchLimit}
|
||||
onChange={this.onFetchLimitOptionChange} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Checkbox
|
||||
label={intl.get("service.removeAd")}
|
||||
checked={this.state.removeAd}
|
||||
onChange={(_, c) => this.setState({removeAd: c})} />
|
||||
<Stack horizontal style={{marginTop: 32}}>
|
||||
<Stack.Item>
|
||||
<PrimaryButton
|
||||
disabled={!this.validateForm()}
|
||||
onClick={this.save}
|
||||
text={this.state.existing ? intl.get("edit") : intl.get("confirm")} />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
{this.state.existing
|
||||
? <DangerButton onClick={this.remove} text={intl.get("delete")} />
|
||||
: <DefaultButton onClick={this.props.exit} text={intl.get("cancel")} />
|
||||
}
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
{ this.state.existing && <LiteExporter serviceConfigs={this.props.configs} /> }
|
||||
</Stack>
|
||||
</>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default InoreaderConfigsTab
|
||||
export default InoreaderConfigsTab
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
import * as React from "react"
|
||||
import intl from "react-intl-universal"
|
||||
import { Stack, ContextualMenuItemType, DefaultButton, IContextualMenuProps, DirectionalHint } from "@fluentui/react"
|
||||
import {
|
||||
Stack,
|
||||
ContextualMenuItemType,
|
||||
DefaultButton,
|
||||
IContextualMenuProps,
|
||||
DirectionalHint,
|
||||
} from "@fluentui/react"
|
||||
import { ServiceConfigs, SyncService } from "../../../schema-types"
|
||||
import { renderShareQR } from "../../context-menu"
|
||||
import { platformCtrl } from "../../../scripts/utils"
|
||||
|
@ -12,9 +18,10 @@ type LiteExporterProps = {
|
|||
serviceConfigs: ServiceConfigs
|
||||
}
|
||||
|
||||
const LEARN_MORE_URL = "https://github.com/yang991178/fluent-reader/wiki/Support#mobile-app"
|
||||
const LEARN_MORE_URL =
|
||||
"https://github.com/yang991178/fluent-reader/wiki/Support#mobile-app"
|
||||
|
||||
const LiteExporter: React.FunctionComponent<LiteExporterProps> = (props) => {
|
||||
const LiteExporter: React.FunctionComponent<LiteExporterProps> = props => {
|
||||
let url = "https://hyliu.me/fr2l/?"
|
||||
const params = new URLSearchParams()
|
||||
switch (props.serviceConfigs.type) {
|
||||
|
@ -58,16 +65,21 @@ const LiteExporter: React.FunctionComponent<LiteExporterProps> = (props) => {
|
|||
key: "openInBrowser",
|
||||
text: intl.get("rules.help"),
|
||||
iconProps: { iconName: "NavigateExternalInline" },
|
||||
onClick: e => { window.utils.openExternal(LEARN_MORE_URL, platformCtrl(e)) }
|
||||
onClick: e => {
|
||||
window.utils.openExternal(LEARN_MORE_URL, platformCtrl(e))
|
||||
},
|
||||
},
|
||||
]
|
||||
],
|
||||
}
|
||||
return <Stack style={{marginTop: 32}}>
|
||||
<DefaultButton
|
||||
text={intl.get("service.exportToLite")}
|
||||
onRenderMenuIcon={() => <></>}
|
||||
menuProps={menuProps} />
|
||||
</Stack>
|
||||
return (
|
||||
<Stack style={{ marginTop: 32 }}>
|
||||
<DefaultButton
|
||||
text={intl.get("service.exportToLite")}
|
||||
onRenderMenuIcon={() => <></>}
|
||||
menuProps={menuProps}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
export default LiteExporter
|
||||
export default LiteExporter
|
||||
|
|
|
@ -1,9 +1,27 @@
|
|||
import * as React from "react"
|
||||
import intl from "react-intl-universal"
|
||||
import { Label, DefaultButton, TextField, Stack, PrimaryButton, DetailsList,
|
||||
IColumn, SelectionMode, Selection, IChoiceGroupOption, ChoiceGroup, IDropdownOption,
|
||||
Dropdown, MessageBar, MessageBarType } from "@fluentui/react"
|
||||
import { SourceState, RSSSource, SourceOpenTarget } from "../../scripts/models/source"
|
||||
import {
|
||||
Label,
|
||||
DefaultButton,
|
||||
TextField,
|
||||
Stack,
|
||||
PrimaryButton,
|
||||
DetailsList,
|
||||
IColumn,
|
||||
SelectionMode,
|
||||
Selection,
|
||||
IChoiceGroupOption,
|
||||
ChoiceGroup,
|
||||
IDropdownOption,
|
||||
Dropdown,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
} from "@fluentui/react"
|
||||
import {
|
||||
SourceState,
|
||||
RSSSource,
|
||||
SourceOpenTarget,
|
||||
} from "../../scripts/models/source"
|
||||
import { urlTest } from "../../scripts/utils"
|
||||
import DangerButton from "../utils/danger-button"
|
||||
|
||||
|
@ -15,7 +33,10 @@ type SourcesTabProps = {
|
|||
addSource: (url: string) => void
|
||||
updateSourceName: (source: RSSSource, name: string) => void
|
||||
updateSourceIcon: (source: RSSSource, iconUrl: string) => Promise<void>
|
||||
updateSourceOpenTarget: (source: RSSSource, target: SourceOpenTarget) => void
|
||||
updateSourceOpenTarget: (
|
||||
source: RSSSource,
|
||||
target: SourceOpenTarget
|
||||
) => void
|
||||
updateFetchFrequency: (source: RSSSource, frequency: number) => void
|
||||
deleteSource: (source: RSSSource) => void
|
||||
deleteSources: (sources: RSSSource[]) => void
|
||||
|
@ -26,7 +47,7 @@ type SourcesTabProps = {
|
|||
type SourcesTabState = {
|
||||
[formName: string]: string
|
||||
} & {
|
||||
selectedSource: RSSSource,
|
||||
selectedSource: RSSSource
|
||||
selectedSources: RSSSource[]
|
||||
}
|
||||
|
||||
|
@ -45,21 +66,23 @@ class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
|
|||
newUrl: "",
|
||||
newSourceName: "",
|
||||
selectedSource: null,
|
||||
selectedSources: null
|
||||
selectedSources: null,
|
||||
}
|
||||
this.selection = new Selection({
|
||||
getKey: s => (s as RSSSource).sid,
|
||||
onSelectionChanged: () => {
|
||||
let count = this.selection.getSelectedCount()
|
||||
let sources = count ? this.selection.getSelection() as RSSSource[] : null
|
||||
let count = this.selection.getSelectedCount()
|
||||
let sources = count
|
||||
? (this.selection.getSelection() as RSSSource[])
|
||||
: null
|
||||
this.setState({
|
||||
selectedSource: count === 1 ? sources[0] : null,
|
||||
selectedSources: count > 1 ? sources : null,
|
||||
newSourceName: count === 1 ? sources[0].name : "",
|
||||
newSourceIcon: count === 1 ? (sources[0].iconurl || "") : "",
|
||||
newSourceIcon: count === 1 ? sources[0].iconurl || "" : "",
|
||||
sourceEditOption: EditDropdownKeys.Name,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -81,25 +104,24 @@ class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
|
|||
iconName: "ImagePixel",
|
||||
minWidth: 16,
|
||||
maxWidth: 16,
|
||||
onRender: (s: RSSSource) => s.iconurl && (
|
||||
<img src={s.iconurl} className="favicon" />
|
||||
)
|
||||
onRender: (s: RSSSource) =>
|
||||
s.iconurl && <img src={s.iconurl} className="favicon" />,
|
||||
},
|
||||
{
|
||||
key: "name",
|
||||
name: intl.get("name"),
|
||||
fieldName: "name",
|
||||
minWidth: 200,
|
||||
data: 'string',
|
||||
isRowHeader: true
|
||||
data: "string",
|
||||
isRowHeader: true,
|
||||
},
|
||||
{
|
||||
key: "url",
|
||||
name: "URL",
|
||||
fieldName: "url",
|
||||
minWidth: 280,
|
||||
data: 'string'
|
||||
}
|
||||
data: "string",
|
||||
},
|
||||
]
|
||||
|
||||
sourceEditOptions = (): IDropdownOption[] => [
|
||||
|
@ -109,7 +131,7 @@ class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
|
|||
]
|
||||
|
||||
onSourceEditOptionChange = (_, option: IDropdownOption) => {
|
||||
this.setState({sourceEditOption: option.key as string})
|
||||
this.setState({ sourceEditOption: option.key as string })
|
||||
}
|
||||
|
||||
fetchFrequencyOptions = (): IDropdownOption[] => [
|
||||
|
@ -121,37 +143,61 @@ class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
|
|||
{ key: "180", text: intl.get("time.hour", { h: 3 }) },
|
||||
{ key: "360", text: intl.get("time.hour", { h: 6 }) },
|
||||
{ key: "720", text: intl.get("time.hour", { h: 12 }) },
|
||||
{ key: "1440", text: intl.get("time.day", { d: 1 }) }
|
||||
{ key: "1440", text: intl.get("time.day", { d: 1 }) },
|
||||
]
|
||||
|
||||
onFetchFrequencyChange = (_, option: IDropdownOption) => {
|
||||
let frequency = parseInt(option.key as string)
|
||||
this.props.updateFetchFrequency(this.state.selectedSource, frequency)
|
||||
this.setState({selectedSource: {...this.state.selectedSource, fetchFrequency: frequency} as RSSSource})
|
||||
this.setState({
|
||||
selectedSource: {
|
||||
...this.state.selectedSource,
|
||||
fetchFrequency: frequency,
|
||||
} as RSSSource,
|
||||
})
|
||||
}
|
||||
|
||||
sourceOpenTargetChoices = (): IChoiceGroupOption[] => [
|
||||
{ key: String(SourceOpenTarget.Local), text: intl.get("sources.rssText") },
|
||||
{ key: String(SourceOpenTarget.FullContent), text: intl.get("article.loadFull") },
|
||||
{ key: String(SourceOpenTarget.Webpage), text: intl.get("sources.loadWebpage") },
|
||||
{ key: String(SourceOpenTarget.External), text: intl.get("openExternal") }
|
||||
{
|
||||
key: String(SourceOpenTarget.Local),
|
||||
text: intl.get("sources.rssText"),
|
||||
},
|
||||
{
|
||||
key: String(SourceOpenTarget.FullContent),
|
||||
text: intl.get("article.loadFull"),
|
||||
},
|
||||
{
|
||||
key: String(SourceOpenTarget.Webpage),
|
||||
text: intl.get("sources.loadWebpage"),
|
||||
},
|
||||
{
|
||||
key: String(SourceOpenTarget.External),
|
||||
text: intl.get("openExternal"),
|
||||
},
|
||||
]
|
||||
|
||||
updateSourceName = () => {
|
||||
let newName = this.state.newSourceName.trim()
|
||||
this.props.updateSourceName(this.state.selectedSource, newName)
|
||||
this.setState({selectedSource: {...this.state.selectedSource, name: newName} as RSSSource})
|
||||
this.setState({
|
||||
selectedSource: {
|
||||
...this.state.selectedSource,
|
||||
name: newName,
|
||||
} as RSSSource,
|
||||
})
|
||||
}
|
||||
|
||||
updateSourceIcon = () => {
|
||||
let newIcon = this.state.newSourceIcon.trim()
|
||||
this.props.updateSourceIcon(this.state.selectedSource, newIcon)
|
||||
this.setState({selectedSource: {...this.state.selectedSource, iconurl: newIcon}})
|
||||
this.setState({
|
||||
selectedSource: { ...this.state.selectedSource, iconurl: newIcon },
|
||||
})
|
||||
}
|
||||
|
||||
handleInputChange = (event) => {
|
||||
handleInputChange = event => {
|
||||
const name: string = event.target.name
|
||||
this.setState({[name]: event.target.value})
|
||||
this.setState({ [name]: event.target.value })
|
||||
}
|
||||
|
||||
addSource = (event: React.FormEvent) => {
|
||||
|
@ -163,164 +209,258 @@ class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
|
|||
onOpenTargetChange = (_, option: IChoiceGroupOption) => {
|
||||
let newTarget = parseInt(option.key) as SourceOpenTarget
|
||||
this.props.updateSourceOpenTarget(this.state.selectedSource, newTarget)
|
||||
this.setState({selectedSource: {...this.state.selectedSource, openTarget: newTarget} as RSSSource})
|
||||
this.setState({
|
||||
selectedSource: {
|
||||
...this.state.selectedSource,
|
||||
openTarget: newTarget,
|
||||
} as RSSSource,
|
||||
})
|
||||
}
|
||||
|
||||
render = () => (
|
||||
<div className="tab-body">
|
||||
{this.props.serviceOn && (
|
||||
<MessageBar messageBarType={MessageBarType.info}>{intl.get("sources.serviceWarning")}</MessageBar>
|
||||
<MessageBar messageBarType={MessageBarType.info}>
|
||||
{intl.get("sources.serviceWarning")}
|
||||
</MessageBar>
|
||||
)}
|
||||
<Label>{intl.get("sources.opmlFile")}</Label>
|
||||
<Stack horizontal>
|
||||
<Stack.Item>
|
||||
<PrimaryButton onClick={this.props.importOPML} text={intl.get("sources.import")} />
|
||||
<PrimaryButton
|
||||
onClick={this.props.importOPML}
|
||||
text={intl.get("sources.import")}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<DefaultButton onClick={this.props.exportOPML} text={intl.get("sources.export")} />
|
||||
<DefaultButton
|
||||
onClick={this.props.exportOPML}
|
||||
text={intl.get("sources.export")}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
|
||||
<form onSubmit={this.addSource}>
|
||||
<Label htmlFor="newUrl">{intl.get("sources.add")}</Label>
|
||||
<Label htmlFor="newUrl">{intl.get("sources.add")}</Label>
|
||||
<Stack horizontal>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
onGetErrorMessage={v => urlTest(v.trim()) ? "" : intl.get("sources.badUrl")}
|
||||
validateOnLoad={false}
|
||||
<TextField
|
||||
onGetErrorMessage={v =>
|
||||
urlTest(v.trim())
|
||||
? ""
|
||||
: intl.get("sources.badUrl")
|
||||
}
|
||||
validateOnLoad={false}
|
||||
placeholder={intl.get("sources.inputUrl")}
|
||||
value={this.state.newUrl}
|
||||
id="newUrl"
|
||||
name="newUrl"
|
||||
onChange={this.handleInputChange} />
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<PrimaryButton
|
||||
<PrimaryButton
|
||||
disabled={!urlTest(this.state.newUrl.trim())}
|
||||
type="submit"
|
||||
text={intl.get("add")} />
|
||||
text={intl.get("add")}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</form>
|
||||
|
||||
<DetailsList
|
||||
compact={Object.keys(this.props.sources).length >= 10}
|
||||
items={Object.values(this.props.sources)}
|
||||
items={Object.values(this.props.sources)}
|
||||
columns={this.columns()}
|
||||
getKey={s => s.sid}
|
||||
setKey="selected"
|
||||
selection={this.selection}
|
||||
selectionMode={SelectionMode.multiple} />
|
||||
selectionMode={SelectionMode.multiple}
|
||||
/>
|
||||
|
||||
{this.state.selectedSource && <>
|
||||
{this.state.selectedSource.serviceRef && (
|
||||
<MessageBar messageBarType={MessageBarType.info}>{intl.get("sources.serviceManaged")}</MessageBar>
|
||||
)}
|
||||
<Label>{intl.get("sources.selected")}</Label>
|
||||
<Stack horizontal>
|
||||
<Stack.Item>
|
||||
<Dropdown
|
||||
options={this.sourceEditOptions()}
|
||||
selectedKey={this.state.sourceEditOption}
|
||||
onChange={this.onSourceEditOptionChange}
|
||||
style={{width: 120}} />
|
||||
</Stack.Item>
|
||||
{this.state.sourceEditOption === EditDropdownKeys.Name && <>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
onGetErrorMessage={v => v.trim().length == 0 ? intl.get("emptyName") : ""}
|
||||
validateOnLoad={false}
|
||||
placeholder={intl.get("sources.name")}
|
||||
value={this.state.newSourceName}
|
||||
name="newSourceName"
|
||||
onChange={this.handleInputChange} />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<DefaultButton
|
||||
disabled={this.state.newSourceName.trim().length == 0}
|
||||
onClick={this.updateSourceName}
|
||||
text={intl.get("sources.editName")} />
|
||||
</Stack.Item>
|
||||
</>}
|
||||
{this.state.sourceEditOption === EditDropdownKeys.Icon && <>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
onGetErrorMessage={v => urlTest(v.trim()) ? "" : intl.get("sources.badUrl")}
|
||||
validateOnLoad={false}
|
||||
placeholder={intl.get("sources.inputUrl")}
|
||||
value={this.state.newSourceIcon}
|
||||
name="newSourceIcon"
|
||||
onChange={this.handleInputChange} />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<DefaultButton
|
||||
disabled={!urlTest(this.state.newSourceIcon.trim())}
|
||||
onClick={this.updateSourceIcon}
|
||||
text={intl.get("edit")} />
|
||||
</Stack.Item>
|
||||
</>}
|
||||
{this.state.sourceEditOption === EditDropdownKeys.Url && <>
|
||||
<Stack.Item grow>
|
||||
<TextField disabled value={this.state.selectedSource.url} />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<DefaultButton
|
||||
onClick={() => window.utils.writeClipboard(this.state.selectedSource.url)}
|
||||
text={intl.get("context.copy")} />
|
||||
</Stack.Item>
|
||||
</>}
|
||||
|
||||
</Stack>
|
||||
{!this.state.selectedSource.serviceRef && <>
|
||||
<Label>{intl.get("sources.fetchFrequency")}</Label>
|
||||
<Stack>
|
||||
<Stack.Item>
|
||||
<Dropdown
|
||||
options={this.fetchFrequencyOptions()}
|
||||
selectedKey={this.state.selectedSource.fetchFrequency ? String(this.state.selectedSource.fetchFrequency) : "0"}
|
||||
onChange={this.onFetchFrequencyChange}
|
||||
style={{width: 200}} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</>}
|
||||
<ChoiceGroup
|
||||
label={intl.get("sources.openTarget")}
|
||||
options={this.sourceOpenTargetChoices()}
|
||||
selectedKey={String(this.state.selectedSource.openTarget)}
|
||||
onChange={this.onOpenTargetChange} />
|
||||
{!this.state.selectedSource.serviceRef && (
|
||||
{this.state.selectedSource && (
|
||||
<>
|
||||
{this.state.selectedSource.serviceRef && (
|
||||
<MessageBar messageBarType={MessageBarType.info}>
|
||||
{intl.get("sources.serviceManaged")}
|
||||
</MessageBar>
|
||||
)}
|
||||
<Label>{intl.get("sources.selected")}</Label>
|
||||
<Stack horizontal>
|
||||
<Stack.Item>
|
||||
<DangerButton
|
||||
onClick={() => this.props.deleteSource(this.state.selectedSource)}
|
||||
key={this.state.selectedSource.sid}
|
||||
text={intl.get("sources.delete")} />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<span className="settings-hint">{intl.get("sources.deleteWarning")}</span>
|
||||
<Dropdown
|
||||
options={this.sourceEditOptions()}
|
||||
selectedKey={this.state.sourceEditOption}
|
||||
onChange={this.onSourceEditOptionChange}
|
||||
style={{ width: 120 }}
|
||||
/>
|
||||
</Stack.Item>
|
||||
{this.state.sourceEditOption ===
|
||||
EditDropdownKeys.Name && (
|
||||
<>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
onGetErrorMessage={v =>
|
||||
v.trim().length == 0
|
||||
? intl.get("emptyName")
|
||||
: ""
|
||||
}
|
||||
validateOnLoad={false}
|
||||
placeholder={intl.get("sources.name")}
|
||||
value={this.state.newSourceName}
|
||||
name="newSourceName"
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<DefaultButton
|
||||
disabled={
|
||||
this.state.newSourceName.trim()
|
||||
.length == 0
|
||||
}
|
||||
onClick={this.updateSourceName}
|
||||
text={intl.get("sources.editName")}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</>
|
||||
)}
|
||||
{this.state.sourceEditOption ===
|
||||
EditDropdownKeys.Icon && (
|
||||
<>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
onGetErrorMessage={v =>
|
||||
urlTest(v.trim())
|
||||
? ""
|
||||
: intl.get("sources.badUrl")
|
||||
}
|
||||
validateOnLoad={false}
|
||||
placeholder={intl.get(
|
||||
"sources.inputUrl"
|
||||
)}
|
||||
value={this.state.newSourceIcon}
|
||||
name="newSourceIcon"
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<DefaultButton
|
||||
disabled={
|
||||
!urlTest(
|
||||
this.state.newSourceIcon.trim()
|
||||
)
|
||||
}
|
||||
onClick={this.updateSourceIcon}
|
||||
text={intl.get("edit")}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</>
|
||||
)}
|
||||
{this.state.sourceEditOption ===
|
||||
EditDropdownKeys.Url && (
|
||||
<>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
disabled
|
||||
value={this.state.selectedSource.url}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<DefaultButton
|
||||
onClick={() =>
|
||||
window.utils.writeClipboard(
|
||||
this.state.selectedSource.url
|
||||
)
|
||||
}
|
||||
text={intl.get("context.copy")}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</>}
|
||||
{this.state.selectedSources && (this.state.selectedSources.filter(s => s.serviceRef).length === 0
|
||||
? <>
|
||||
<Label>{intl.get("sources.selectedMulti")}</Label>
|
||||
<Stack horizontal>
|
||||
<Stack.Item>
|
||||
<DangerButton
|
||||
onClick={() => this.props.deleteSources(this.state.selectedSources)}
|
||||
text={intl.get("sources.delete")} />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<span className="settings-hint">{intl.get("sources.deleteWarning")}</span>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</>
|
||||
: (
|
||||
<MessageBar messageBarType={MessageBarType.info}>{intl.get("sources.serviceManaged")}</MessageBar>
|
||||
))}
|
||||
{!this.state.selectedSource.serviceRef && (
|
||||
<>
|
||||
<Label>{intl.get("sources.fetchFrequency")}</Label>
|
||||
<Stack>
|
||||
<Stack.Item>
|
||||
<Dropdown
|
||||
options={this.fetchFrequencyOptions()}
|
||||
selectedKey={
|
||||
this.state.selectedSource
|
||||
.fetchFrequency
|
||||
? String(
|
||||
this.state.selectedSource
|
||||
.fetchFrequency
|
||||
)
|
||||
: "0"
|
||||
}
|
||||
onChange={this.onFetchFrequencyChange}
|
||||
style={{ width: 200 }}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
<ChoiceGroup
|
||||
label={intl.get("sources.openTarget")}
|
||||
options={this.sourceOpenTargetChoices()}
|
||||
selectedKey={String(
|
||||
this.state.selectedSource.openTarget
|
||||
)}
|
||||
onChange={this.onOpenTargetChange}
|
||||
/>
|
||||
{!this.state.selectedSource.serviceRef && (
|
||||
<Stack horizontal>
|
||||
<Stack.Item>
|
||||
<DangerButton
|
||||
onClick={() =>
|
||||
this.props.deleteSource(
|
||||
this.state.selectedSource
|
||||
)
|
||||
}
|
||||
key={this.state.selectedSource.sid}
|
||||
text={intl.get("sources.delete")}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<span className="settings-hint">
|
||||
{intl.get("sources.deleteWarning")}
|
||||
</span>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{this.state.selectedSources &&
|
||||
(this.state.selectedSources.filter(s => s.serviceRef).length ===
|
||||
0 ? (
|
||||
<>
|
||||
<Label>{intl.get("sources.selectedMulti")}</Label>
|
||||
<Stack horizontal>
|
||||
<Stack.Item>
|
||||
<DangerButton
|
||||
onClick={() =>
|
||||
this.props.deleteSources(
|
||||
this.state.selectedSources
|
||||
)
|
||||
}
|
||||
text={intl.get("sources.delete")}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<span className="settings-hint">
|
||||
{intl.get("sources.deleteWarning")}
|
||||
</span>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</>
|
||||
) : (
|
||||
<MessageBar messageBarType={MessageBarType.info}>
|
||||
{intl.get("sources.serviceManaged")}
|
||||
</MessageBar>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SourcesTab
|
||||
export default SourcesTab
|
||||
|
|
|
@ -70,8 +70,8 @@ declare class ResizeObserver {
|
|||
*
|
||||
* resizeObserver.observe(divElem);
|
||||
*/
|
||||
constructor(callback: ResizeObserverCallback);
|
||||
|
||||
constructor(callback: ResizeObserverCallback)
|
||||
|
||||
/**
|
||||
* The **disconnect()** method of the
|
||||
* [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)
|
||||
|
@ -80,8 +80,8 @@ declare class ResizeObserver {
|
|||
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement)
|
||||
* targets.
|
||||
*/
|
||||
disconnect: () => void;
|
||||
|
||||
disconnect: () => void
|
||||
|
||||
/**
|
||||
* The `observe()` method of the
|
||||
* [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)
|
||||
|
@ -102,8 +102,8 @@ declare class ResizeObserver {
|
|||
* An options object allowing you to set options for the observation.
|
||||
* Currently this only has one possible option that can be set.
|
||||
*/
|
||||
observe: (target: Element, options?: ResizeObserverObserveOptions) => void;
|
||||
|
||||
observe: (target: Element, options?: ResizeObserverObserveOptions) => void
|
||||
|
||||
/**
|
||||
* The **unobserve()** method of the
|
||||
* [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)
|
||||
|
@ -111,86 +111,86 @@ declare class ResizeObserver {
|
|||
* [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or
|
||||
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement).
|
||||
*/
|
||||
unobserve: (target: Element) => void;
|
||||
}
|
||||
|
||||
interface ResizeObserverObserveOptions {
|
||||
unobserve: (target: Element) => void
|
||||
}
|
||||
|
||||
interface ResizeObserverObserveOptions {
|
||||
/**
|
||||
* Sets which box model the observer will observe changes to. Possible values
|
||||
* are `content-box` (the default), and `border-box`.
|
||||
*
|
||||
* @default "content-box"
|
||||
*/
|
||||
box?: "content-box" | "border-box";
|
||||
}
|
||||
|
||||
/**
|
||||
* The function called whenever an observed resize occurs. The function is
|
||||
* called with two parameters:
|
||||
*
|
||||
* @param entries
|
||||
* An array of
|
||||
* [ResizeObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry)
|
||||
* objects that can be used to access the new dimensions of the element after
|
||||
* each change.
|
||||
*
|
||||
* @param observer
|
||||
* A reference to the `ResizeObserver` itself, so it will definitely be
|
||||
* accessible from inside the callback, should you need it. This could be used
|
||||
* for example to automatically unobserve the observer when a certain condition
|
||||
* is reached, but you can omit it if you don't need it.
|
||||
*
|
||||
* The callback will generally follow a pattern along the lines of:
|
||||
* @example
|
||||
* function(entries, observer) {
|
||||
* for (let entry of entries) {
|
||||
* // Do something to each entry
|
||||
* // and possibly something to the observer itself
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @example
|
||||
* const resizeObserver = new ResizeObserver(entries => {
|
||||
* for (let entry of entries) {
|
||||
* if(entry.contentBoxSize) {
|
||||
* h1Elem.style.fontSize = Math.max(1.5, entry.contentBoxSize.inlineSize/200) + 'rem';
|
||||
* pElem.style.fontSize = Math.max(1, entry.contentBoxSize.inlineSize/600) + 'rem';
|
||||
* } else {
|
||||
* h1Elem.style.fontSize = Math.max(1.5, entry.contentRect.width/200) + 'rem';
|
||||
* pElem.style.fontSize = Math.max(1, entry.contentRect.width/600) + 'rem';
|
||||
* }
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* resizeObserver.observe(divElem);
|
||||
*/
|
||||
type ResizeObserverCallback = (
|
||||
box?: "content-box" | "border-box"
|
||||
}
|
||||
|
||||
/**
|
||||
* The function called whenever an observed resize occurs. The function is
|
||||
* called with two parameters:
|
||||
*
|
||||
* @param entries
|
||||
* An array of
|
||||
* [ResizeObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry)
|
||||
* objects that can be used to access the new dimensions of the element after
|
||||
* each change.
|
||||
*
|
||||
* @param observer
|
||||
* A reference to the `ResizeObserver` itself, so it will definitely be
|
||||
* accessible from inside the callback, should you need it. This could be used
|
||||
* for example to automatically unobserve the observer when a certain condition
|
||||
* is reached, but you can omit it if you don't need it.
|
||||
*
|
||||
* The callback will generally follow a pattern along the lines of:
|
||||
* @example
|
||||
* function(entries, observer) {
|
||||
* for (let entry of entries) {
|
||||
* // Do something to each entry
|
||||
* // and possibly something to the observer itself
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @example
|
||||
* const resizeObserver = new ResizeObserver(entries => {
|
||||
* for (let entry of entries) {
|
||||
* if(entry.contentBoxSize) {
|
||||
* h1Elem.style.fontSize = Math.max(1.5, entry.contentBoxSize.inlineSize/200) + 'rem';
|
||||
* pElem.style.fontSize = Math.max(1, entry.contentBoxSize.inlineSize/600) + 'rem';
|
||||
* } else {
|
||||
* h1Elem.style.fontSize = Math.max(1.5, entry.contentRect.width/200) + 'rem';
|
||||
* pElem.style.fontSize = Math.max(1, entry.contentRect.width/600) + 'rem';
|
||||
* }
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* resizeObserver.observe(divElem);
|
||||
*/
|
||||
type ResizeObserverCallback = (
|
||||
entries: ResizeObserverEntry[],
|
||||
observer: ResizeObserver,
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* The **ResizeObserverEntry** interface represents the object passed to the
|
||||
* [ResizeObserver()](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/ResizeObserver)
|
||||
* constructor's callback function, which allows you to access the new
|
||||
* dimensions of the
|
||||
* [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or
|
||||
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement)
|
||||
* being observed.
|
||||
*/
|
||||
interface ResizeObserverEntry {
|
||||
observer: ResizeObserver
|
||||
) => void
|
||||
|
||||
/**
|
||||
* The **ResizeObserverEntry** interface represents the object passed to the
|
||||
* [ResizeObserver()](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/ResizeObserver)
|
||||
* constructor's callback function, which allows you to access the new
|
||||
* dimensions of the
|
||||
* [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or
|
||||
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement)
|
||||
* being observed.
|
||||
*/
|
||||
interface ResizeObserverEntry {
|
||||
/**
|
||||
* An object containing the new border box size of the observed element when
|
||||
* the callback is run.
|
||||
*/
|
||||
readonly borderBoxSize: ResizeObserverEntryBoxSize;
|
||||
|
||||
readonly borderBoxSize: ResizeObserverEntryBoxSize
|
||||
|
||||
/**
|
||||
* An object containing the new content box size of the observed element when
|
||||
* the callback is run.
|
||||
*/
|
||||
readonly contentBoxSize: ResizeObserverEntryBoxSize;
|
||||
|
||||
readonly contentBoxSize: ResizeObserverEntryBoxSize
|
||||
|
||||
/**
|
||||
* A [DOMRectReadOnly](https://developer.mozilla.org/en-US/docs/Web/API/DOMRectReadOnly)
|
||||
* object containing the new size of the observed element when the callback is
|
||||
|
@ -200,24 +200,24 @@ declare class ResizeObserver {
|
|||
* in future versions.
|
||||
*/
|
||||
// node_modules/typescript/lib/lib.dom.d.ts
|
||||
readonly contentRect: DOMRectReadOnly;
|
||||
|
||||
readonly contentRect: DOMRectReadOnly
|
||||
|
||||
/**
|
||||
* A reference to the
|
||||
* [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or
|
||||
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement)
|
||||
* being observed.
|
||||
*/
|
||||
readonly target: Element;
|
||||
}
|
||||
|
||||
/**
|
||||
* The **borderBoxSize** read-only property of the
|
||||
* [ResizeObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry)
|
||||
* interface returns an object containing the new border box size of the
|
||||
* observed element when the callback is run.
|
||||
*/
|
||||
interface ResizeObserverEntryBoxSize {
|
||||
readonly target: Element
|
||||
}
|
||||
|
||||
/**
|
||||
* The **borderBoxSize** read-only property of the
|
||||
* [ResizeObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry)
|
||||
* interface returns an object containing the new border box size of the
|
||||
* observed element when the callback is run.
|
||||
*/
|
||||
interface ResizeObserverEntryBoxSize {
|
||||
/**
|
||||
* The length of the observed element's border box in the block dimension. For
|
||||
* boxes with a horizontal
|
||||
|
@ -225,8 +225,8 @@ declare class ResizeObserver {
|
|||
* this is the vertical dimension, or height; if the writing-mode is vertical,
|
||||
* this is the horizontal dimension, or width.
|
||||
*/
|
||||
blockSize: number;
|
||||
|
||||
blockSize: number
|
||||
|
||||
/**
|
||||
* The length of the observed element's border box in the inline dimension.
|
||||
* For boxes with a horizontal
|
||||
|
@ -234,9 +234,9 @@ declare class ResizeObserver {
|
|||
* this is the horizontal dimension, or width; if the writing-mode is
|
||||
* vertical, this is the vertical dimension, or height.
|
||||
*/
|
||||
inlineSize: number;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
ResizeObserver: typeof ResizeObserver;
|
||||
}
|
||||
inlineSize: number
|
||||
}
|
||||
|
||||
interface Window {
|
||||
ResizeObserver: typeof ResizeObserver
|
||||
}
|
||||
|
|
|
@ -43,19 +43,22 @@ class ArticleSearch extends React.Component<SearchProps, SearchState> {
|
|||
}
|
||||
|
||||
render() {
|
||||
return this.props.searchOn && (
|
||||
<SearchBox
|
||||
componentRef={this.inputRef}
|
||||
className="article-search"
|
||||
placeholder={intl.get("search")}
|
||||
value={this.state.query}
|
||||
onChange={this.onSearchChange} />
|
||||
return (
|
||||
this.props.searchOn && (
|
||||
<SearchBox
|
||||
componentRef={this.inputRef}
|
||||
className="article-search"
|
||||
placeholder={intl.get("search")}
|
||||
value={this.state.query}
|
||||
onChange={this.onSearchChange}
|
||||
/>
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const getSearchProps = (state: RootState) => ({
|
||||
searchOn: state.page.searchOn,
|
||||
initQuery: state.page.filter.search
|
||||
initQuery: state.page.filter.search,
|
||||
})
|
||||
export default connect(getSearchProps)(ArticleSearch)
|
||||
export default connect(getSearchProps)(ArticleSearch)
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import * as React from "react"
|
||||
import intl from "react-intl-universal"
|
||||
import { PrimaryButton } from "@fluentui/react";
|
||||
import { PrimaryButton } from "@fluentui/react"
|
||||
|
||||
class DangerButton extends PrimaryButton {
|
||||
timerID: NodeJS.Timeout
|
||||
|
||||
state = {
|
||||
confirming: false
|
||||
confirming: false,
|
||||
}
|
||||
|
||||
clear = () => {
|
||||
|
@ -34,15 +34,20 @@ class DangerButton extends PrimaryButton {
|
|||
}
|
||||
|
||||
render = () => (
|
||||
<PrimaryButton
|
||||
{...this.props}
|
||||
<PrimaryButton
|
||||
{...this.props}
|
||||
className={this.props.className + " danger"}
|
||||
onClick={this.onClick}
|
||||
text={this.state.confirming ? intl.get("dangerButton", { action: this.props.text.toLowerCase() }) : this.props.text}
|
||||
>
|
||||
text={
|
||||
this.state.confirming
|
||||
? intl.get("dangerButton", {
|
||||
action: this.props.text.toLowerCase(),
|
||||
})
|
||||
: this.props.text
|
||||
}>
|
||||
{this.props.children}
|
||||
</PrimaryButton>
|
||||
)
|
||||
}
|
||||
|
||||
export default DangerButton
|
||||
export default DangerButton
|
||||
|
|
|
@ -10,18 +10,15 @@ class Time extends React.Component<TimeProps> {
|
|||
state = { now: new Date() }
|
||||
|
||||
componentDidMount() {
|
||||
this.timerID = setInterval(
|
||||
() => this.tick(),
|
||||
60000
|
||||
);
|
||||
}
|
||||
|
||||
this.timerID = setInterval(() => this.tick(), 60000)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.timerID)
|
||||
}
|
||||
|
||||
tick() {
|
||||
this.setState({ now: new Date() });
|
||||
this.setState({ now: new Date() })
|
||||
}
|
||||
|
||||
displayTime(past: Date, now: Date): string {
|
||||
|
@ -35,9 +32,11 @@ class Time extends React.Component<TimeProps> {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<span className="time">{ this.displayTime(this.props.date, this.state.now) }</span>
|
||||
<span className="time">
|
||||
{this.displayTime(this.props.date, this.state.now)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Time
|
||||
export default Time
|
||||
|
|
|
@ -1,18 +1,31 @@
|
|||
import { connect } from "react-redux"
|
||||
import { createSelector } from "reselect"
|
||||
import { RootState } from "../scripts/reducer"
|
||||
import { RSSItem, markUnread, markRead, toggleStarred, toggleHidden, itemShortcuts } from "../scripts/models/item"
|
||||
import {
|
||||
RSSItem,
|
||||
markUnread,
|
||||
markRead,
|
||||
toggleStarred,
|
||||
toggleHidden,
|
||||
itemShortcuts,
|
||||
} from "../scripts/models/item"
|
||||
import { AppDispatch } from "../scripts/utils"
|
||||
import { dismissItem, showOffsetItem } from "../scripts/models/page"
|
||||
import Article from "../components/article"
|
||||
import { openTextMenu, closeContextMenu, openImageMenu } from "../scripts/models/app"
|
||||
import {
|
||||
openTextMenu,
|
||||
closeContextMenu,
|
||||
openImageMenu,
|
||||
} from "../scripts/models/app"
|
||||
|
||||
type ArticleContainerProps = {
|
||||
itemId: number
|
||||
}
|
||||
|
||||
const getItem = (state: RootState, props: ArticleContainerProps) => state.items[props.itemId]
|
||||
const getSource = (state: RootState, props: ArticleContainerProps) => state.sources[state.items[props.itemId].source]
|
||||
const getItem = (state: RootState, props: ArticleContainerProps) =>
|
||||
state.items[props.itemId]
|
||||
const getSource = (state: RootState, props: ArticleContainerProps) =>
|
||||
state.sources[state.items[props.itemId].source]
|
||||
const getLocale = (state: RootState) => state.app.locale
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
|
@ -21,28 +34,35 @@ const makeMapStateToProps = () => {
|
|||
(item, source, locale) => ({
|
||||
item: item,
|
||||
source: source,
|
||||
locale: locale
|
||||
locale: locale,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch: AppDispatch) => {
|
||||
return {
|
||||
shortcuts: (item: RSSItem, e: KeyboardEvent) => dispatch(itemShortcuts(item, e)),
|
||||
shortcuts: (item: RSSItem, e: KeyboardEvent) =>
|
||||
dispatch(itemShortcuts(item, e)),
|
||||
dismiss: () => dispatch(dismissItem()),
|
||||
offsetItem: (offset: number) => dispatch(showOffsetItem(offset)),
|
||||
toggleHasRead: (item: RSSItem) => dispatch(item.hasRead ? markUnread(item) : markRead(item)),
|
||||
toggleHasRead: (item: RSSItem) =>
|
||||
dispatch(item.hasRead ? markUnread(item) : markRead(item)),
|
||||
toggleStarred: (item: RSSItem) => dispatch(toggleStarred(item)),
|
||||
toggleHidden: (item: RSSItem) => {
|
||||
if (!item.hidden) dispatch(dismissItem())
|
||||
if (!item.hasRead && !item.hidden) dispatch(markRead(item))
|
||||
dispatch(toggleHidden(item))
|
||||
},
|
||||
textMenu: (position: [number, number], text: string, url: string) => dispatch(openTextMenu(position, text, url)),
|
||||
imageMenu: (position: [number, number]) => dispatch(openImageMenu(position)),
|
||||
dismissContextMenu: () => dispatch(closeContextMenu())
|
||||
textMenu: (position: [number, number], text: string, url: string) =>
|
||||
dispatch(openTextMenu(position, text, url)),
|
||||
imageMenu: (position: [number, number]) =>
|
||||
dispatch(openImageMenu(position)),
|
||||
dismissContextMenu: () => dispatch(closeContextMenu()),
|
||||
}
|
||||
}
|
||||
|
||||
const ArticleContainer = connect(makeMapStateToProps, mapDispatchToProps)(Article)
|
||||
export default ArticleContainer
|
||||
const ArticleContainer = connect(
|
||||
makeMapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Article)
|
||||
export default ArticleContainer
|
||||
|
|
|
@ -1,10 +1,28 @@
|
|||
import { connect } from "react-redux"
|
||||
import { createSelector } from "reselect"
|
||||
import { RootState } from "../scripts/reducer"
|
||||
import { ContextMenuType, closeContextMenu, toggleSettings } from "../scripts/models/app"
|
||||
import {
|
||||
ContextMenuType,
|
||||
closeContextMenu,
|
||||
toggleSettings,
|
||||
} from "../scripts/models/app"
|
||||
import { ContextMenu } from "../components/context-menu"
|
||||
import { RSSItem, markRead, markUnread, toggleStarred, toggleHidden, markAllRead, fetchItems } from "../scripts/models/item"
|
||||
import { showItem, switchView, switchFilter, toggleFilter, setViewConfigs } from "../scripts/models/page"
|
||||
import {
|
||||
RSSItem,
|
||||
markRead,
|
||||
markUnread,
|
||||
toggleStarred,
|
||||
toggleHidden,
|
||||
markAllRead,
|
||||
fetchItems,
|
||||
} from "../scripts/models/item"
|
||||
import {
|
||||
showItem,
|
||||
switchView,
|
||||
switchFilter,
|
||||
toggleFilter,
|
||||
setViewConfigs,
|
||||
} from "../scripts/models/page"
|
||||
import { ViewType, ViewConfigs } from "../schema-types"
|
||||
import { FilterType } from "../scripts/models/feed"
|
||||
|
||||
|
@ -17,51 +35,59 @@ const mapStateToProps = createSelector(
|
|||
[getContext, getViewType, getFilter, getViewConfigs],
|
||||
(context, viewType, filter, viewConfigs) => {
|
||||
switch (context.type) {
|
||||
case ContextMenuType.Item: return {
|
||||
type: context.type,
|
||||
event: context.event,
|
||||
viewConfigs: viewConfigs,
|
||||
item: context.target[0],
|
||||
feedId: context.target[1]
|
||||
}
|
||||
case ContextMenuType.Text: return {
|
||||
type: context.type,
|
||||
position: context.position,
|
||||
text: context.target[0],
|
||||
url: context.target[1]
|
||||
}
|
||||
case ContextMenuType.View: return {
|
||||
type: context.type,
|
||||
event: context.event,
|
||||
viewType: viewType,
|
||||
filter: filter.type
|
||||
}
|
||||
case ContextMenuType.Group: return {
|
||||
type: context.type,
|
||||
event: context.event,
|
||||
sids: context.target
|
||||
}
|
||||
case ContextMenuType.Image: return {
|
||||
type: context.type,
|
||||
position: context.position
|
||||
}
|
||||
case ContextMenuType.MarkRead: return {
|
||||
type: context.type,
|
||||
event: context.event
|
||||
}
|
||||
default: return { type: ContextMenuType.Hidden }
|
||||
case ContextMenuType.Item:
|
||||
return {
|
||||
type: context.type,
|
||||
event: context.event,
|
||||
viewConfigs: viewConfigs,
|
||||
item: context.target[0],
|
||||
feedId: context.target[1],
|
||||
}
|
||||
case ContextMenuType.Text:
|
||||
return {
|
||||
type: context.type,
|
||||
position: context.position,
|
||||
text: context.target[0],
|
||||
url: context.target[1],
|
||||
}
|
||||
case ContextMenuType.View:
|
||||
return {
|
||||
type: context.type,
|
||||
event: context.event,
|
||||
viewType: viewType,
|
||||
filter: filter.type,
|
||||
}
|
||||
case ContextMenuType.Group:
|
||||
return {
|
||||
type: context.type,
|
||||
event: context.event,
|
||||
sids: context.target,
|
||||
}
|
||||
case ContextMenuType.Image:
|
||||
return {
|
||||
type: context.type,
|
||||
position: context.position,
|
||||
}
|
||||
case ContextMenuType.MarkRead:
|
||||
return {
|
||||
type: context.type,
|
||||
event: context.event,
|
||||
}
|
||||
default:
|
||||
return { type: ContextMenuType.Hidden }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
showItem: (feedId: string, item: RSSItem) => dispatch(showItem(feedId, item)),
|
||||
showItem: (feedId: string, item: RSSItem) =>
|
||||
dispatch(showItem(feedId, item)),
|
||||
markRead: (item: RSSItem) => dispatch(markRead(item)),
|
||||
markUnread: (item: RSSItem) => dispatch(markUnread(item)),
|
||||
toggleStarred: (item: RSSItem) => dispatch(toggleStarred(item)),
|
||||
toggleHidden: (item: RSSItem) => {
|
||||
if(!item.hasRead) {
|
||||
if (!item.hasRead) {
|
||||
dispatch(markRead(item))
|
||||
item.hasRead = true // get around chaining error
|
||||
}
|
||||
|
@ -71,7 +97,8 @@ const mapDispatchToProps = dispatch => {
|
|||
window.settings.setDefaultView(viewType)
|
||||
dispatch(switchView(viewType))
|
||||
},
|
||||
setViewConfigs: (configs: ViewConfigs) => dispatch(setViewConfigs(configs)),
|
||||
setViewConfigs: (configs: ViewConfigs) =>
|
||||
dispatch(setViewConfigs(configs)),
|
||||
switchFilter: (filter: FilterType) => dispatch(switchFilter(filter)),
|
||||
toggleFilter: (filter: FilterType) => dispatch(toggleFilter(filter)),
|
||||
markAllRead: (sids?: number[], date?: Date, before?: boolean) => {
|
||||
|
@ -79,10 +106,10 @@ const mapDispatchToProps = dispatch => {
|
|||
},
|
||||
fetchItems: (sids: number[]) => dispatch(fetchItems(false, sids)),
|
||||
settings: (sids: number[]) => dispatch(toggleSettings(true, sids)),
|
||||
close: () => dispatch(closeContextMenu())
|
||||
close: () => dispatch(closeContextMenu()),
|
||||
}
|
||||
}
|
||||
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps)
|
||||
export type ContextReduxProps = typeof connector
|
||||
export const ContextMenuContainer = connector(ContextMenu)
|
||||
export const ContextMenuContainer = connector(ContextMenu)
|
||||
|
|
|
@ -15,7 +15,8 @@ interface FeedContainerProps {
|
|||
|
||||
const getSources = (state: RootState) => state.sources
|
||||
const getItems = (state: RootState) => state.items
|
||||
const getFeed = (state: RootState, props: FeedContainerProps) => state.feeds[props.feedId]
|
||||
const getFeed = (state: RootState, props: FeedContainerProps) =>
|
||||
state.feeds[props.feedId]
|
||||
const getFilter = (state: RootState) => state.page.filter
|
||||
const getView = (_, props: FeedContainerProps) => props.viewType
|
||||
const getViewConfigs = (state: RootState) => state.page.viewConfigs
|
||||
|
@ -23,7 +24,15 @@ const getCurrentItem = (state: RootState) => state.page.itemId
|
|||
|
||||
const makeMapStateToProps = () => {
|
||||
return createSelector(
|
||||
[getSources, getItems, getFeed, getView, getFilter, getViewConfigs, getCurrentItem],
|
||||
[
|
||||
getSources,
|
||||
getItems,
|
||||
getFeed,
|
||||
getView,
|
||||
getFilter,
|
||||
getViewConfigs,
|
||||
getCurrentItem,
|
||||
],
|
||||
(sources, items, feed, viewType, filter, viewConfigs, currentItem) => ({
|
||||
feed: feed,
|
||||
items: feed.iids.map(iid => items[iid]),
|
||||
|
@ -37,14 +46,16 @@ const makeMapStateToProps = () => {
|
|||
}
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
shortcuts: (item: RSSItem, e: KeyboardEvent) => dispatch(itemShortcuts(item, e)),
|
||||
shortcuts: (item: RSSItem, e: KeyboardEvent) =>
|
||||
dispatch(itemShortcuts(item, e)),
|
||||
markRead: (item: RSSItem) => dispatch(markRead(item)),
|
||||
contextMenu: (feedId: string, item: RSSItem, e) => dispatch(openItemMenu(item, feedId, e)),
|
||||
contextMenu: (feedId: string, item: RSSItem, e) =>
|
||||
dispatch(openItemMenu(item, feedId, e)),
|
||||
loadMore: (feed: RSSFeed) => dispatch(loadMore(feed)),
|
||||
showItem: (fid: string, item: RSSItem) => dispatch(showItem(fid, item))
|
||||
showItem: (fid: string, item: RSSItem) => dispatch(showItem(fid, item)),
|
||||
}
|
||||
}
|
||||
|
||||
const connector = connect(makeMapStateToProps, mapDispatchToProps)
|
||||
export type FeedReduxProps = typeof connector
|
||||
export const FeedContainer = connector(Feed)
|
||||
export const FeedContainer = connector(Feed)
|
||||
|
|
|
@ -10,11 +10,11 @@ const getLogs = (state: RootState) => state.app.logMenu
|
|||
const mapStateToProps = createSelector(getLogs, logs => logs)
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
return {
|
||||
close: () => dispatch(toggleLogMenu()),
|
||||
showItem: (iid: number) => dispatch(showItemFromId(iid))
|
||||
showItem: (iid: number) => dispatch(showItemFromId(iid)),
|
||||
}
|
||||
}
|
||||
|
||||
const LogMenuContainer = connect(mapStateToProps, mapDispatchToProps)(LogMenu)
|
||||
export default LogMenuContainer
|
||||
export default LogMenuContainer
|
||||
|
|
|
@ -5,7 +5,11 @@ import { Menu } from "../components/menu"
|
|||
import { toggleMenu, openGroupMenu } from "../scripts/models/app"
|
||||
import { toggleGroupExpansion } from "../scripts/models/group"
|
||||
import { SourceGroup } from "../schema-types"
|
||||
import { selectAllArticles, selectSources, toggleSearch } from "../scripts/models/page"
|
||||
import {
|
||||
selectAllArticles,
|
||||
selectSources,
|
||||
toggleSearch,
|
||||
} from "../scripts/models/page"
|
||||
import { ViewType } from "../schema-types"
|
||||
import { initFeeds } from "../scripts/models/feed"
|
||||
import { RSSSource } from "../scripts/models/source"
|
||||
|
@ -14,7 +18,8 @@ const getApp = (state: RootState) => state.app
|
|||
const getSources = (state: RootState) => state.sources
|
||||
const getGroups = (state: RootState) => state.groups
|
||||
const getSearchOn = (state: RootState) => state.page.searchOn
|
||||
const getItemOn = (state: RootState) => state.page.itemId !== null && state.page.viewType !== ViewType.List
|
||||
const getItemOn = (state: RootState) =>
|
||||
state.page.itemId !== null && state.page.viewType !== ViewType.List
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
[getApp, getSources, getGroups, getSearchOn, getItemOn],
|
||||
|
@ -32,21 +37,24 @@ const mapStateToProps = createSelector(
|
|||
const mapDispatchToProps = dispatch => ({
|
||||
toggleMenu: () => dispatch(toggleMenu()),
|
||||
allArticles: (init = false) => {
|
||||
dispatch(selectAllArticles(init)),
|
||||
dispatch(initFeeds())
|
||||
dispatch(selectAllArticles(init)), dispatch(initFeeds())
|
||||
},
|
||||
selectSourceGroup: (group: SourceGroup, menuKey: string) => {
|
||||
dispatch(selectSources(group.sids, menuKey, group.name))
|
||||
dispatch(initFeeds())
|
||||
},
|
||||
selectSource: (source: RSSSource) => {
|
||||
dispatch(selectSources([source.sid], "s-"+source.sid, source.name))
|
||||
dispatch(selectSources([source.sid], "s-" + source.sid, source.name))
|
||||
dispatch(initFeeds())
|
||||
},
|
||||
groupContextMenu: (sids: number[], event: React.MouseEvent) => {
|
||||
dispatch(openGroupMenu(sids, event))
|
||||
},
|
||||
updateGroupExpansion: (event: React.MouseEvent<HTMLElement>, key: string, selected: string) => {
|
||||
updateGroupExpansion: (
|
||||
event: React.MouseEvent<HTMLElement>,
|
||||
key: string,
|
||||
selected: string
|
||||
) => {
|
||||
if ((event.target as HTMLElement).tagName === "I" || key === selected) {
|
||||
let [type, index] = key.split("-")
|
||||
if (type === "g") dispatch(toggleGroupExpansion(parseInt(index)))
|
||||
|
@ -56,4 +64,4 @@ const mapDispatchToProps = dispatch => ({
|
|||
})
|
||||
|
||||
const MenuContainer = connect(mapStateToProps, mapDispatchToProps)(Menu)
|
||||
export default MenuContainer
|
||||
export default MenuContainer
|
||||
|
|
|
@ -3,31 +3,38 @@ import { connect } from "react-redux"
|
|||
import { createSelector } from "reselect"
|
||||
import { RootState } from "../scripts/reducer"
|
||||
import { fetchItems, markAllRead } from "../scripts/models/item"
|
||||
import { toggleMenu, toggleLogMenu, toggleSettings, openViewMenu, openMarkAllMenu } from "../scripts/models/app"
|
||||
import {
|
||||
toggleMenu,
|
||||
toggleLogMenu,
|
||||
toggleSettings,
|
||||
openViewMenu,
|
||||
openMarkAllMenu,
|
||||
} from "../scripts/models/app"
|
||||
import { toggleSearch } from "../scripts/models/page"
|
||||
import { ViewType } from "../schema-types"
|
||||
import Nav from "../components/nav"
|
||||
|
||||
const getState = (state: RootState) => state.app
|
||||
const getItemShown = (state: RootState) => state.page.itemId && state.page.viewType !== ViewType.List
|
||||
const getItemShown = (state: RootState) =>
|
||||
state.page.itemId && state.page.viewType !== ViewType.List
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
[getState, getItemShown],
|
||||
[getState, getItemShown],
|
||||
(state, itemShown) => ({
|
||||
state: state,
|
||||
itemShown: itemShown
|
||||
}
|
||||
))
|
||||
itemShown: itemShown,
|
||||
})
|
||||
)
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
fetch: () => dispatch(fetchItems()),
|
||||
menu: () => dispatch(toggleMenu()),
|
||||
logs: () => dispatch(toggleLogMenu()),
|
||||
views: () => dispatch(openViewMenu()),
|
||||
settings: () => dispatch(toggleSettings()),
|
||||
search: () => dispatch(toggleSearch()),
|
||||
markAllRead: () => dispatch(openMarkAllMenu())
|
||||
markAllRead: () => dispatch(openMarkAllMenu()),
|
||||
})
|
||||
|
||||
const NavContainer = connect(mapStateToProps, mapDispatchToProps)(Nav)
|
||||
export default NavContainer
|
||||
export default NavContainer
|
||||
|
|
|
@ -9,7 +9,8 @@ import { ContextMenuType } from "../scripts/models/app"
|
|||
const getPage = (state: RootState) => state.page
|
||||
const getSettings = (state: RootState) => state.app.settings.display
|
||||
const getMenu = (state: RootState) => state.app.menu
|
||||
const getContext = (state: RootState) => state.app.contextMenu.type != ContextMenuType.Hidden
|
||||
const getContext = (state: RootState) =>
|
||||
state.app.contextMenu.type != ContextMenuType.Hidden
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
[getPage, getSettings, getMenu, getContext],
|
||||
|
@ -20,14 +21,14 @@ const mapStateToProps = createSelector(
|
|||
contextOn: contextOn,
|
||||
itemId: page.itemId,
|
||||
itemFromFeed: page.itemFromFeed,
|
||||
viewType: page.viewType
|
||||
viewType: page.viewType,
|
||||
})
|
||||
)
|
||||
|
||||
const mapDispatchToProps = (dispatch: AppDispatch) => ({
|
||||
dismissItem: () => dispatch(dismissItem()),
|
||||
offsetItem: (offset: number) => dispatch(showOffsetItem(offset))
|
||||
offsetItem: (offset: number) => dispatch(showOffsetItem(offset)),
|
||||
})
|
||||
|
||||
const PageContainer = connect(mapStateToProps, mapDispatchToProps)(Page)
|
||||
export default PageContainer
|
||||
export default PageContainer
|
||||
|
|
|
@ -1,24 +1,26 @@
|
|||
import { connect } from "react-redux"
|
||||
import { createSelector } from "reselect"
|
||||
import { RootState } from "../scripts/reducer"
|
||||
import { exitSettings} from "../scripts/models/app"
|
||||
import { exitSettings } from "../scripts/models/app"
|
||||
import Settings from "../components/settings"
|
||||
|
||||
const getApp = (state: RootState) => state.app
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
[getApp],
|
||||
(app) => ({
|
||||
const mapStateToProps = createSelector([getApp], app => ({
|
||||
display: app.settings.display,
|
||||
blocked: !app.sourceInit || app.syncing || app.fetchingItems || app.settings.saving,
|
||||
exitting: app.settings.saving
|
||||
blocked:
|
||||
!app.sourceInit ||
|
||||
app.syncing ||
|
||||
app.fetchingItems ||
|
||||
app.settings.saving,
|
||||
exitting: app.settings.saving,
|
||||
}))
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
close: () => dispatch(exitSettings())
|
||||
return {
|
||||
close: () => dispatch(exitSettings()),
|
||||
}
|
||||
}
|
||||
|
||||
const SettingsContainer = connect(mapStateToProps, mapDispatchToProps)(Settings)
|
||||
export default SettingsContainer
|
||||
export default SettingsContainer
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import { connect } from "react-redux"
|
||||
import { initIntl, saveSettings, setupAutoFetch } from "../../scripts/models/app"
|
||||
import {
|
||||
initIntl,
|
||||
saveSettings,
|
||||
setupAutoFetch,
|
||||
} from "../../scripts/models/app"
|
||||
import * as db from "../../scripts/db"
|
||||
import AppTab from "../../components/settings/app"
|
||||
import { importAll } from "../../scripts/settings"
|
||||
|
@ -19,16 +23,20 @@ const mapDispatchToProps = (dispatch: AppDispatch) => ({
|
|||
dispatch(saveSettings())
|
||||
let date = new Date()
|
||||
date.setTime(date.getTime() - days * 86400000)
|
||||
await db.itemsDB.delete().from(db.items).where(db.items.date.lt(date)).exec()
|
||||
await db.itemsDB
|
||||
.delete()
|
||||
.from(db.items)
|
||||
.where(db.items.date.lt(date))
|
||||
.exec()
|
||||
await dispatch(updateUnreadCounts())
|
||||
dispatch(saveSettings())
|
||||
},
|
||||
importAll: async () => {
|
||||
dispatch(saveSettings())
|
||||
let cancelled = await importAll()
|
||||
let cancelled = await importAll()
|
||||
if (cancelled) dispatch(saveSettings())
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const AppTabContainer = connect(null, mapDispatchToProps)(AppTab)
|
||||
export default AppTabContainer
|
||||
export default AppTabContainer
|
||||
|
|
|
@ -2,15 +2,22 @@ import { connect } from "react-redux"
|
|||
import { createSelector } from "reselect"
|
||||
import { RootState } from "../../scripts/reducer"
|
||||
import GroupsTab from "../../components/settings/groups"
|
||||
import { createSourceGroup, updateSourceGroup, addSourceToGroup,
|
||||
deleteSourceGroup, removeSourceFromGroup, reorderSourceGroups } from "../../scripts/models/group"
|
||||
import {
|
||||
createSourceGroup,
|
||||
updateSourceGroup,
|
||||
addSourceToGroup,
|
||||
deleteSourceGroup,
|
||||
removeSourceFromGroup,
|
||||
reorderSourceGroups,
|
||||
} from "../../scripts/models/group"
|
||||
import { SourceGroup, SyncService } from "../../schema-types"
|
||||
import { importGroups } from "../../scripts/models/service"
|
||||
import { AppDispatch } from "../../scripts/utils"
|
||||
|
||||
const getSources = (state: RootState) => state.sources
|
||||
const getGroups = (state: RootState) => state.groups
|
||||
const getServiceOn = (state: RootState) => state.service.type !== SyncService.None
|
||||
const getServiceOn = (state: RootState) =>
|
||||
state.service.type !== SyncService.None
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
[getSources, getGroups, getServiceOn],
|
||||
|
@ -18,19 +25,26 @@ const mapStateToProps = createSelector(
|
|||
sources: sources,
|
||||
groups: groups.map((g, i) => ({ ...g, index: i })),
|
||||
serviceOn: serviceOn,
|
||||
key: groups.length
|
||||
key: groups.length,
|
||||
})
|
||||
)
|
||||
|
||||
const mapDispatchToProps = (dispatch: AppDispatch) => ({
|
||||
createGroup: (name: string) => dispatch(createSourceGroup(name)),
|
||||
updateGroup: (group: SourceGroup) => dispatch(updateSourceGroup(group)),
|
||||
addToGroup: (groupIndex: number, sid: number) => dispatch(addSourceToGroup(groupIndex, sid)),
|
||||
deleteGroup: (groupIndex: number) => dispatch(deleteSourceGroup(groupIndex)),
|
||||
removeFromGroup: (groupIndex: number, sids: number[]) => dispatch(removeSourceFromGroup(groupIndex, sids)),
|
||||
reorderGroups: (groups: SourceGroup[]) => dispatch(reorderSourceGroups(groups)),
|
||||
addToGroup: (groupIndex: number, sid: number) =>
|
||||
dispatch(addSourceToGroup(groupIndex, sid)),
|
||||
deleteGroup: (groupIndex: number) =>
|
||||
dispatch(deleteSourceGroup(groupIndex)),
|
||||
removeFromGroup: (groupIndex: number, sids: number[]) =>
|
||||
dispatch(removeSourceFromGroup(groupIndex, sids)),
|
||||
reorderGroups: (groups: SourceGroup[]) =>
|
||||
dispatch(reorderSourceGroups(groups)),
|
||||
importGroups: () => dispatch(importGroups()),
|
||||
})
|
||||
|
||||
const GroupsTabContainer = connect(mapStateToProps, mapDispatchToProps)(GroupsTab)
|
||||
export default GroupsTabContainer
|
||||
const GroupsTabContainer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(GroupsTab)
|
||||
export default GroupsTabContainer
|
||||
|
|
|
@ -8,19 +8,16 @@ import { SourceRule } from "../../scripts/models/rule"
|
|||
|
||||
const getSources = (state: RootState) => state.sources
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
[getSources],
|
||||
(sources) => ({
|
||||
sources: sources
|
||||
})
|
||||
)
|
||||
const mapStateToProps = createSelector([getSources], sources => ({
|
||||
sources: sources,
|
||||
}))
|
||||
|
||||
const mapDispatchToProps = (dispatch: AppDispatch) => ({
|
||||
updateSourceRules: (source: RSSSource, rules: SourceRule[]) => {
|
||||
source.rules = rules
|
||||
dispatch(updateSource(source))
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const RulesTabContainer = connect(mapStateToProps, mapDispatchToProps)(RulesTab)
|
||||
export default RulesTabContainer
|
||||
export default RulesTabContainer
|
||||
|
|
|
@ -4,17 +4,19 @@ import { RootState } from "../../scripts/reducer"
|
|||
import { ServiceTab } from "../../components/settings/service"
|
||||
import { AppDispatch } from "../../scripts/utils"
|
||||
import { ServiceConfigs } from "../../schema-types"
|
||||
import { saveServiceConfigs, getServiceHooksFromType, removeService, syncWithService } from "../../scripts/models/service"
|
||||
import {
|
||||
saveServiceConfigs,
|
||||
getServiceHooksFromType,
|
||||
removeService,
|
||||
syncWithService,
|
||||
} from "../../scripts/models/service"
|
||||
import { saveSettings } from "../../scripts/models/app"
|
||||
|
||||
const getService = (state: RootState) => state.service
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
[getService],
|
||||
(service) => ({
|
||||
configs: service
|
||||
})
|
||||
)
|
||||
const mapStateToProps = createSelector([getService], service => ({
|
||||
configs: service,
|
||||
}))
|
||||
|
||||
const mapDispatchToProps = (dispatch: AppDispatch) => ({
|
||||
save: (configs: ServiceConfigs) => dispatch(saveServiceConfigs(configs)),
|
||||
|
@ -34,8 +36,11 @@ const mapDispatchToProps = (dispatch: AppDispatch) => ({
|
|||
console.log(err)
|
||||
return configs
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const ServiceTabContainer = connect(mapStateToProps, mapDispatchToProps)(ServiceTab)
|
||||
export default ServiceTabContainer
|
||||
const ServiceTabContainer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ServiceTab)
|
||||
export default ServiceTabContainer
|
||||
|
|
|
@ -3,14 +3,22 @@ import { connect } from "react-redux"
|
|||
import { createSelector } from "reselect"
|
||||
import { RootState } from "../../scripts/reducer"
|
||||
import SourcesTab from "../../components/settings/sources"
|
||||
import { addSource, RSSSource, updateSource, deleteSource, SourceOpenTarget, deleteSources } from "../../scripts/models/source"
|
||||
import {
|
||||
addSource,
|
||||
RSSSource,
|
||||
updateSource,
|
||||
deleteSource,
|
||||
SourceOpenTarget,
|
||||
deleteSources,
|
||||
} from "../../scripts/models/source"
|
||||
import { importOPML, exportOPML } from "../../scripts/models/group"
|
||||
import { AppDispatch, validateFavicon } from "../../scripts/utils"
|
||||
import { saveSettings, toggleSettings } from "../../scripts/models/app"
|
||||
import { SyncService } from "../../schema-types"
|
||||
|
||||
const getSources = (state: RootState) => state.sources
|
||||
const getServiceOn = (state: RootState) => state.service.type !== SyncService.None
|
||||
const getServiceOn = (state: RootState) =>
|
||||
state.service.type !== SyncService.None
|
||||
const getSIDs = (state: RootState) => state.app.settings.sids
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
|
@ -23,7 +31,7 @@ const mapStateToProps = createSelector(
|
|||
)
|
||||
|
||||
const mapDispatchToProps = (dispatch: AppDispatch) => {
|
||||
return {
|
||||
return {
|
||||
acknowledgeSIDs: () => dispatch(toggleSettings(true)),
|
||||
addSource: (url: string) => dispatch(addSource(url)),
|
||||
updateSourceName: (source: RSSSource, name: string) => {
|
||||
|
@ -38,18 +46,32 @@ const mapDispatchToProps = (dispatch: AppDispatch) => {
|
|||
}
|
||||
dispatch(saveSettings())
|
||||
},
|
||||
updateSourceOpenTarget: (source: RSSSource, target: SourceOpenTarget) => {
|
||||
dispatch(updateSource({ ...source, openTarget: target } as RSSSource))
|
||||
updateSourceOpenTarget: (
|
||||
source: RSSSource,
|
||||
target: SourceOpenTarget
|
||||
) => {
|
||||
dispatch(
|
||||
updateSource({ ...source, openTarget: target } as RSSSource)
|
||||
)
|
||||
},
|
||||
updateFetchFrequency: (source: RSSSource, frequency: number) => {
|
||||
dispatch(updateSource({ ...source, fetchFrequency: frequency } as RSSSource))
|
||||
dispatch(
|
||||
updateSource({
|
||||
...source,
|
||||
fetchFrequency: frequency,
|
||||
} as RSSSource)
|
||||
)
|
||||
},
|
||||
deleteSource: (source: RSSSource) => dispatch(deleteSource(source)),
|
||||
deleteSources: (sources: RSSSource[]) => dispatch(deleteSources(sources)),
|
||||
deleteSources: (sources: RSSSource[]) =>
|
||||
dispatch(deleteSources(sources)),
|
||||
importOPML: () => dispatch(importOPML()),
|
||||
exportOPML: () => dispatch(exportOPML())
|
||||
exportOPML: () => dispatch(exportOPML()),
|
||||
}
|
||||
}
|
||||
|
||||
const SourcesTabContainer = connect(mapStateToProps, mapDispatchToProps)(SourcesTab)
|
||||
export default SourcesTabContainer
|
||||
const SourcesTabContainer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(SourcesTab)
|
||||
export default SourcesTabContainer
|
||||
|
|
|
@ -12,7 +12,8 @@ if (!process.mas) {
|
|||
}
|
||||
|
||||
if (!app.isPackaged) app.setAppUserModelId(process.execPath)
|
||||
else if (process.platform === "win32") app.setAppUserModelId("me.hyliu.fluentreader")
|
||||
else if (process.platform === "win32")
|
||||
app.setAppUserModelId("me.hyliu.fluentreader")
|
||||
|
||||
let restarting = false
|
||||
|
||||
|
@ -28,29 +29,74 @@ if (process.platform === "darwin") {
|
|||
{
|
||||
label: "Application",
|
||||
submenu: [
|
||||
{ label: "Hide", accelerator:"Command+H", click: () => { app.hide() } },
|
||||
{ label: "Quit", accelerator: "Command+Q", click: () => { if (winManager.hasWindow) winManager.mainWindow.close() } }
|
||||
]
|
||||
{
|
||||
label: "Hide",
|
||||
accelerator: "Command+H",
|
||||
click: () => {
|
||||
app.hide()
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Quit",
|
||||
accelerator: "Command+Q",
|
||||
click: () => {
|
||||
if (winManager.hasWindow) winManager.mainWindow.close()
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Edit",
|
||||
submenu: [
|
||||
{ label: "Undo", accelerator: "CmdOrCtrl+Z", selector: "undo:" },
|
||||
{ label: "Redo", accelerator: "Shift+CmdOrCtrl+Z", selector: "redo:" },
|
||||
{
|
||||
label: "Undo",
|
||||
accelerator: "CmdOrCtrl+Z",
|
||||
selector: "undo:",
|
||||
},
|
||||
{
|
||||
label: "Redo",
|
||||
accelerator: "Shift+CmdOrCtrl+Z",
|
||||
selector: "redo:",
|
||||
},
|
||||
{ label: "Cut", accelerator: "CmdOrCtrl+X", selector: "cut:" },
|
||||
{ label: "Copy", accelerator: "CmdOrCtrl+C", selector: "copy:" },
|
||||
{ label: "Paste", accelerator: "CmdOrCtrl+V", selector: "paste:" },
|
||||
{ label: "Select All", accelerator: "CmdOrCtrl+A", selector: "selectAll:" }
|
||||
]
|
||||
{
|
||||
label: "Copy",
|
||||
accelerator: "CmdOrCtrl+C",
|
||||
selector: "copy:",
|
||||
},
|
||||
{
|
||||
label: "Paste",
|
||||
accelerator: "CmdOrCtrl+V",
|
||||
selector: "paste:",
|
||||
},
|
||||
{
|
||||
label: "Select All",
|
||||
accelerator: "CmdOrCtrl+A",
|
||||
selector: "selectAll:",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Window",
|
||||
submenu: [
|
||||
{ label: "Close", accelerator: "Command+W", click: () => { if (winManager.hasWindow) winManager.mainWindow.close() } },
|
||||
{ label: "Minimize", accelerator: "Command+M", click: () => { if (winManager.hasWindow()) winManager.mainWindow.minimize() } },
|
||||
{ label: "Zoom", click: () => winManager.zoom() }
|
||||
]
|
||||
}
|
||||
{
|
||||
label: "Close",
|
||||
accelerator: "Command+W",
|
||||
click: () => {
|
||||
if (winManager.hasWindow) winManager.mainWindow.close()
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Minimize",
|
||||
accelerator: "Command+M",
|
||||
click: () => {
|
||||
if (winManager.hasWindow())
|
||||
winManager.mainWindow.minimize()
|
||||
},
|
||||
},
|
||||
{ label: "Zoom", click: () => winManager.zoom() },
|
||||
],
|
||||
},
|
||||
]
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(template))
|
||||
} else {
|
||||
|
@ -61,7 +107,9 @@ const winManager = new WindowManager()
|
|||
|
||||
app.on("window-all-closed", () => {
|
||||
if (winManager.hasWindow()) {
|
||||
winManager.mainWindow.webContents.session.clearStorageData({ storages: ["cookies", "localstorage"] })
|
||||
winManager.mainWindow.webContents.session.clearStorageData({
|
||||
storages: ["cookies", "localstorage"],
|
||||
})
|
||||
}
|
||||
winManager.mainWindow = null
|
||||
if (restarting) {
|
||||
|
@ -81,7 +129,10 @@ ipcMain.handle("import-all-settings", (_, configs: SchemaTypes) => {
|
|||
}
|
||||
performUpdate(store)
|
||||
nativeTheme.themeSource = store.get("theme", ThemeSettings.Default)
|
||||
setTimeout(() => {
|
||||
winManager.mainWindow.close()
|
||||
}, process.platform === "darwin" ? 1000 : 0); // Why ???
|
||||
setTimeout(
|
||||
() => {
|
||||
winManager.mainWindow.close()
|
||||
},
|
||||
process.platform === "darwin" ? 1000 : 0
|
||||
) // Why ???
|
||||
})
|
||||
|
|
|
@ -31,4 +31,4 @@ ReactDOM.render(
|
|||
<Root />
|
||||
</Provider>,
|
||||
document.getElementById("app")
|
||||
)
|
||||
)
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
import Store = require("electron-store")
|
||||
import { SchemaTypes, SourceGroup, ViewType, ThemeSettings, SearchEngines,
|
||||
SyncService, ServiceConfigs, ViewConfigs } from "../schema-types"
|
||||
import {
|
||||
SchemaTypes,
|
||||
SourceGroup,
|
||||
ViewType,
|
||||
ThemeSettings,
|
||||
SearchEngines,
|
||||
SyncService,
|
||||
ServiceConfigs,
|
||||
ViewConfigs,
|
||||
} from "../schema-types"
|
||||
import { ipcMain, session, nativeTheme, app } from "electron"
|
||||
import { WindowManager } from "./window"
|
||||
|
||||
|
@ -10,12 +18,12 @@ const GROUPS_STORE_KEY = "sourceGroups"
|
|||
ipcMain.handle("set-groups", (_, groups: SourceGroup[]) => {
|
||||
store.set(GROUPS_STORE_KEY, groups)
|
||||
})
|
||||
ipcMain.on("get-groups", (event) => {
|
||||
ipcMain.on("get-groups", event => {
|
||||
event.returnValue = store.get(GROUPS_STORE_KEY, [])
|
||||
})
|
||||
|
||||
const MENU_STORE_KEY = "menuOn"
|
||||
ipcMain.on("get-menu", (event) => {
|
||||
ipcMain.on("get-menu", event => {
|
||||
event.returnValue = store.get(MENU_STORE_KEY, false)
|
||||
})
|
||||
ipcMain.handle("set-menu", (_, state: boolean) => {
|
||||
|
@ -46,13 +54,13 @@ function setProxy(address = null) {
|
|||
session.fromPartition("sandbox").setProxy(rules)
|
||||
}
|
||||
}
|
||||
ipcMain.on("get-proxy-status", (event) => {
|
||||
ipcMain.on("get-proxy-status", event => {
|
||||
event.returnValue = getProxyStatus()
|
||||
})
|
||||
ipcMain.on("toggle-proxy-status", () => {
|
||||
toggleProxyStatus()
|
||||
})
|
||||
ipcMain.on("get-proxy", (event) => {
|
||||
ipcMain.on("get-proxy", event => {
|
||||
event.returnValue = getProxy()
|
||||
})
|
||||
ipcMain.handle("set-proxy", (_, address = null) => {
|
||||
|
@ -60,7 +68,7 @@ ipcMain.handle("set-proxy", (_, address = null) => {
|
|||
})
|
||||
|
||||
const VIEW_STORE_KEY = "view"
|
||||
ipcMain.on("get-view", (event) => {
|
||||
ipcMain.on("get-view", event => {
|
||||
event.returnValue = store.get(VIEW_STORE_KEY, ViewType.Cards)
|
||||
})
|
||||
ipcMain.handle("set-view", (_, viewType: ViewType) => {
|
||||
|
@ -68,14 +76,14 @@ ipcMain.handle("set-view", (_, viewType: ViewType) => {
|
|||
})
|
||||
|
||||
const THEME_STORE_KEY = "theme"
|
||||
ipcMain.on("get-theme", (event) => {
|
||||
ipcMain.on("get-theme", event => {
|
||||
event.returnValue = store.get(THEME_STORE_KEY, ThemeSettings.Default)
|
||||
})
|
||||
ipcMain.handle("set-theme", (_, theme: ThemeSettings) => {
|
||||
store.set(THEME_STORE_KEY, theme)
|
||||
nativeTheme.themeSource = theme
|
||||
})
|
||||
ipcMain.on("get-theme-dark-color", (event) => {
|
||||
ipcMain.on("get-theme-dark-color", event => {
|
||||
event.returnValue = nativeTheme.shouldUseDarkColors
|
||||
})
|
||||
export function setThemeListener(manager: WindowManager) {
|
||||
|
@ -97,24 +105,24 @@ ipcMain.handle("set-locale", (_, option: string) => {
|
|||
function getLocaleSettings() {
|
||||
return store.get(LOCALE_STORE_KEY, "default")
|
||||
}
|
||||
ipcMain.on("get-locale-settings", (event) => {
|
||||
ipcMain.on("get-locale-settings", event => {
|
||||
event.returnValue = getLocaleSettings()
|
||||
})
|
||||
ipcMain.on("get-locale", (event) => {
|
||||
ipcMain.on("get-locale", event => {
|
||||
let setting = getLocaleSettings()
|
||||
let locale = setting === "default" ? app.getLocale() : setting
|
||||
event.returnValue = locale
|
||||
})
|
||||
|
||||
const FONT_SIZE_STORE_KEY = "fontSize"
|
||||
ipcMain.on("get-font-size", (event) => {
|
||||
ipcMain.on("get-font-size", event => {
|
||||
event.returnValue = store.get(FONT_SIZE_STORE_KEY, 16)
|
||||
})
|
||||
ipcMain.handle("set-font-size", (_, size: number) => {
|
||||
store.set(FONT_SIZE_STORE_KEY, size)
|
||||
})
|
||||
|
||||
ipcMain.on("get-all-settings", (event) => {
|
||||
ipcMain.on("get-all-settings", event => {
|
||||
let output = {}
|
||||
for (let [key, value] of store) {
|
||||
output[key] = value
|
||||
|
@ -123,7 +131,7 @@ ipcMain.on("get-all-settings", (event) => {
|
|||
})
|
||||
|
||||
const FETCH_INTEVAL_STORE_KEY = "fetchInterval"
|
||||
ipcMain.on("get-fetch-interval", (event) => {
|
||||
ipcMain.on("get-fetch-interval", event => {
|
||||
event.returnValue = store.get(FETCH_INTEVAL_STORE_KEY, 0)
|
||||
})
|
||||
ipcMain.handle("set-fetch-interval", (_, interval: number) => {
|
||||
|
@ -131,7 +139,7 @@ ipcMain.handle("set-fetch-interval", (_, interval: number) => {
|
|||
})
|
||||
|
||||
const SEARCH_ENGINE_STORE_KEY = "searchEngine"
|
||||
ipcMain.on("get-search-engine", (event) => {
|
||||
ipcMain.on("get-search-engine", event => {
|
||||
event.returnValue = store.get(SEARCH_ENGINE_STORE_KEY, SearchEngines.Google)
|
||||
})
|
||||
ipcMain.handle("set-search-engine", (_, engine: SearchEngines) => {
|
||||
|
@ -139,15 +147,17 @@ ipcMain.handle("set-search-engine", (_, engine: SearchEngines) => {
|
|||
})
|
||||
|
||||
const SERVICE_CONFIGS_STORE_KEY = "serviceConfigs"
|
||||
ipcMain.on("get-service-configs", (event) => {
|
||||
event.returnValue = store.get(SERVICE_CONFIGS_STORE_KEY, { type: SyncService.None })
|
||||
ipcMain.on("get-service-configs", event => {
|
||||
event.returnValue = store.get(SERVICE_CONFIGS_STORE_KEY, {
|
||||
type: SyncService.None,
|
||||
})
|
||||
})
|
||||
ipcMain.handle("set-service-configs", (_, configs: ServiceConfigs) => {
|
||||
store.set(SERVICE_CONFIGS_STORE_KEY, configs)
|
||||
})
|
||||
|
||||
const FILTER_TYPE_STORE_KEY = "filterType"
|
||||
ipcMain.on("get-filter-type", (event) => {
|
||||
ipcMain.on("get-filter-type", event => {
|
||||
event.returnValue = store.get(FILTER_TYPE_STORE_KEY, null)
|
||||
})
|
||||
ipcMain.handle("set-filter-type", (_, filterType: number) => {
|
||||
|
@ -158,23 +168,29 @@ const LIST_CONFIGS_STORE_KEY = "listViewConfigs"
|
|||
ipcMain.on("get-view-configs", (event, view: ViewType) => {
|
||||
switch (view) {
|
||||
case ViewType.List:
|
||||
event.returnValue = store.get(LIST_CONFIGS_STORE_KEY, ViewConfigs.ShowCover)
|
||||
event.returnValue = store.get(
|
||||
LIST_CONFIGS_STORE_KEY,
|
||||
ViewConfigs.ShowCover
|
||||
)
|
||||
break
|
||||
default:
|
||||
event.returnValue = undefined
|
||||
break
|
||||
}
|
||||
})
|
||||
ipcMain.handle("set-view-configs", (_, view: ViewType, configs: ViewConfigs) => {
|
||||
switch (view) {
|
||||
case ViewType.List:
|
||||
store.set(LIST_CONFIGS_STORE_KEY, configs)
|
||||
break
|
||||
ipcMain.handle(
|
||||
"set-view-configs",
|
||||
(_, view: ViewType, configs: ViewConfigs) => {
|
||||
switch (view) {
|
||||
case ViewType.List:
|
||||
store.set(LIST_CONFIGS_STORE_KEY, configs)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const NEDB_STATUS_STORE_KEY = "useNeDB"
|
||||
ipcMain.on("get-nedb-status", (event) => {
|
||||
ipcMain.on("get-nedb-status", event => {
|
||||
event.returnValue = store.get(NEDB_STATUS_STORE_KEY, true)
|
||||
})
|
||||
ipcMain.handle("set-nedb-status", (_, flag: boolean) => {
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import { TouchBarTexts } from "../schema-types"
|
||||
import { BrowserWindow, TouchBar } from "electron"
|
||||
|
||||
|
||||
function createTouchBarFunctionButton(window: BrowserWindow, text: string, key: string) {
|
||||
function createTouchBarFunctionButton(
|
||||
window: BrowserWindow,
|
||||
text: string,
|
||||
key: string
|
||||
) {
|
||||
return new TouchBar.TouchBarButton({
|
||||
label: text,
|
||||
click: () => window.webContents.send("touchbar-event", key)
|
||||
click: () => window.webContents.send("touchbar-event", key),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -18,7 +21,7 @@ export function initMainTouchBar(texts: TouchBarTexts, window: BrowserWindow) {
|
|||
createTouchBarFunctionButton(window, texts.refresh, "F5"),
|
||||
createTouchBarFunctionButton(window, texts.markAll, "F6"),
|
||||
createTouchBarFunctionButton(window, texts.notifications, "F7"),
|
||||
]
|
||||
],
|
||||
})
|
||||
window.setTouchBar(touchBar)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,10 @@ export default function performUpdate(store: Store<SchemaTypes>) {
|
|||
if (useNeDB === undefined) {
|
||||
if (version !== null) {
|
||||
const revs = version.split(".").map(s => parseInt(s))
|
||||
store.set("useNeDB", (revs[0] === 0 && revs[1] < 8) || !app.isPackaged)
|
||||
store.set(
|
||||
"useNeDB",
|
||||
(revs[0] === 0 && revs[1] < 8) || !app.isPackaged
|
||||
)
|
||||
} else {
|
||||
store.set("useNeDB", false)
|
||||
}
|
||||
|
@ -18,4 +21,4 @@ export default function performUpdate(store: Store<SchemaTypes>) {
|
|||
if (version != currentVersion) {
|
||||
store.set("version", currentVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
import { ipcMain, shell, dialog, app, session, clipboard, TouchBar } from "electron"
|
||||
import {
|
||||
ipcMain,
|
||||
shell,
|
||||
dialog,
|
||||
app,
|
||||
session,
|
||||
clipboard,
|
||||
TouchBar,
|
||||
} from "electron"
|
||||
import { WindowManager } from "./window"
|
||||
import fs = require("fs")
|
||||
import { ImageCallbackTypes, TouchBarTexts } from "../schema-types"
|
||||
import { initMainTouchBar } from "./touchbar"
|
||||
|
||||
export function setUtilsListeners(manager: WindowManager) {
|
||||
async function openExternal(url: string, background=false) {
|
||||
async function openExternal(url: string, background = false) {
|
||||
if (url.startsWith("https://") || url.startsWith("http://")) {
|
||||
if (background && process.platform === "darwin") {
|
||||
shell.openExternal(url, { activate: false })
|
||||
|
@ -18,21 +26,21 @@ 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()
|
||||
if (contents.getType() === "webview") openExternal(url, disposition === "background-tab")
|
||||
if (contents.getType() === "webview")
|
||||
openExternal(url, disposition === "background-tab")
|
||||
})
|
||||
contents.on("will-navigate", (event, url) => {
|
||||
event.preventDefault()
|
||||
if (contents.getType() === "webview") openExternal(url)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
ipcMain.on("get-version", (event) => {
|
||||
ipcMain.on("get-version", event => {
|
||||
event.returnValue = app.getVersion()
|
||||
})
|
||||
|
||||
|
@ -44,57 +52,76 @@ export function setUtilsListeners(manager: WindowManager) {
|
|||
dialog.showErrorBox(title, content)
|
||||
})
|
||||
|
||||
ipcMain.handle("show-message-box", async (_, title, message, confirm, cancel, defaultCancel, type) => {
|
||||
if (manager.hasWindow()) {
|
||||
let response = await dialog.showMessageBox(manager.mainWindow, {
|
||||
type: type,
|
||||
title: title,
|
||||
message: message,
|
||||
buttons: process.platform === "win32" ? ["Yes", "No"] : [confirm, cancel],
|
||||
cancelId: 1,
|
||||
defaultId: defaultCancel ? 1 : 0
|
||||
})
|
||||
return response.response === 0
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle("show-save-dialog", async (_, filters: Electron.FileFilter[], path: string) => {
|
||||
ipcMain.removeAllListeners("write-save-result")
|
||||
if (manager.hasWindow()) {
|
||||
let response = await dialog.showSaveDialog(manager.mainWindow, {
|
||||
defaultPath: path,
|
||||
filters: filters
|
||||
})
|
||||
if (!response.canceled) {
|
||||
ipcMain.handleOnce("write-save-result", (_, result, errmsg) => {
|
||||
fs.writeFile(response.filePath, result, (err) => {
|
||||
if (err) dialog.showErrorBox(errmsg, String(err))
|
||||
})
|
||||
ipcMain.handle(
|
||||
"show-message-box",
|
||||
async (_, title, message, confirm, cancel, defaultCancel, type) => {
|
||||
if (manager.hasWindow()) {
|
||||
let response = await dialog.showMessageBox(manager.mainWindow, {
|
||||
type: type,
|
||||
title: title,
|
||||
message: message,
|
||||
buttons:
|
||||
process.platform === "win32"
|
||||
? ["Yes", "No"]
|
||||
: [confirm, cancel],
|
||||
cancelId: 1,
|
||||
defaultId: defaultCancel ? 1 : 0,
|
||||
})
|
||||
return true
|
||||
return response.response === 0
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
)
|
||||
|
||||
ipcMain.handle("show-open-dialog", async (_, filters: Electron.FileFilter[]) => {
|
||||
if (manager.hasWindow()) {
|
||||
let response = await dialog.showOpenDialog(manager.mainWindow, {
|
||||
filters: filters,
|
||||
properties: ["openFile"]
|
||||
})
|
||||
if (!response.canceled) {
|
||||
try {
|
||||
return await fs.promises.readFile(response.filePaths[0], "utf-8")
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
ipcMain.handle(
|
||||
"show-save-dialog",
|
||||
async (_, filters: Electron.FileFilter[], path: string) => {
|
||||
ipcMain.removeAllListeners("write-save-result")
|
||||
if (manager.hasWindow()) {
|
||||
let response = await dialog.showSaveDialog(manager.mainWindow, {
|
||||
defaultPath: path,
|
||||
filters: filters,
|
||||
})
|
||||
if (!response.canceled) {
|
||||
ipcMain.handleOnce(
|
||||
"write-save-result",
|
||||
(_, result, errmsg) => {
|
||||
fs.writeFile(response.filePath, result, err => {
|
||||
if (err)
|
||||
dialog.showErrorBox(errmsg, String(err))
|
||||
})
|
||||
}
|
||||
)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
return null
|
||||
})
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
"show-open-dialog",
|
||||
async (_, filters: Electron.FileFilter[]) => {
|
||||
if (manager.hasWindow()) {
|
||||
let response = await dialog.showOpenDialog(manager.mainWindow, {
|
||||
filters: filters,
|
||||
properties: ["openFile"],
|
||||
})
|
||||
if (!response.canceled) {
|
||||
try {
|
||||
return await fs.promises.readFile(
|
||||
response.filePaths[0],
|
||||
"utf-8"
|
||||
)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle("get-cache", async () => {
|
||||
return await session.defaultSession.getCacheSize()
|
||||
|
@ -106,37 +133,67 @@ export function setUtilsListeners(manager: WindowManager) {
|
|||
|
||||
app.on("web-contents-created", (_, contents) => {
|
||||
if (contents.getType() === "webview") {
|
||||
contents.on("did-fail-load", (event, code, desc, validated, isMainFrame) => {
|
||||
if (isMainFrame && manager.hasWindow()) {
|
||||
manager.mainWindow.webContents.send("webview-error", desc)
|
||||
contents.on(
|
||||
"did-fail-load",
|
||||
(event, code, desc, validated, isMainFrame) => {
|
||||
if (isMainFrame && manager.hasWindow()) {
|
||||
manager.mainWindow.webContents.send(
|
||||
"webview-error",
|
||||
desc
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
contents.on("context-menu", (_, params) => {
|
||||
if ((params.hasImageContents || params.selectionText || params.linkURL) && manager.hasWindow()) {
|
||||
if (
|
||||
(params.hasImageContents ||
|
||||
params.selectionText ||
|
||||
params.linkURL) &&
|
||||
manager.hasWindow()
|
||||
) {
|
||||
if (params.hasImageContents) {
|
||||
ipcMain.removeHandler("image-callback")
|
||||
ipcMain.handleOnce("image-callback", (_, type: ImageCallbackTypes) => {
|
||||
switch (type) {
|
||||
case ImageCallbackTypes.OpenExternal:
|
||||
case ImageCallbackTypes.OpenExternalBg:
|
||||
openExternal(params.srcURL, type === ImageCallbackTypes.OpenExternalBg)
|
||||
break
|
||||
case ImageCallbackTypes.SaveAs:
|
||||
contents.session.downloadURL(params.srcURL)
|
||||
break
|
||||
case ImageCallbackTypes.Copy:
|
||||
contents.copyImageAt(params.x, params.y)
|
||||
break
|
||||
case ImageCallbackTypes.CopyLink:
|
||||
clipboard.writeText(params.srcURL)
|
||||
break
|
||||
ipcMain.handleOnce(
|
||||
"image-callback",
|
||||
(_, type: ImageCallbackTypes) => {
|
||||
switch (type) {
|
||||
case ImageCallbackTypes.OpenExternal:
|
||||
case ImageCallbackTypes.OpenExternalBg:
|
||||
openExternal(
|
||||
params.srcURL,
|
||||
type ===
|
||||
ImageCallbackTypes.OpenExternalBg
|
||||
)
|
||||
break
|
||||
case ImageCallbackTypes.SaveAs:
|
||||
contents.session.downloadURL(
|
||||
params.srcURL
|
||||
)
|
||||
break
|
||||
case ImageCallbackTypes.Copy:
|
||||
contents.copyImageAt(params.x, params.y)
|
||||
break
|
||||
case ImageCallbackTypes.CopyLink:
|
||||
clipboard.writeText(params.srcURL)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
manager.mainWindow.webContents.send("webview-context-menu", [params.x, params.y])
|
||||
)
|
||||
manager.mainWindow.webContents.send(
|
||||
"webview-context-menu",
|
||||
[params.x, params.y]
|
||||
)
|
||||
} else {
|
||||
manager.mainWindow.webContents.send("webview-context-menu", [params.x, params.y], params.selectionText, params.linkURL)
|
||||
manager.mainWindow.webContents.send(
|
||||
"webview-context-menu",
|
||||
[params.x, params.y],
|
||||
params.selectionText,
|
||||
params.linkURL
|
||||
)
|
||||
}
|
||||
contents.executeJavaScript(`new Promise(resolve => {
|
||||
contents
|
||||
.executeJavaScript(
|
||||
`new Promise(resolve => {
|
||||
const dismiss = () => {
|
||||
document.removeEventListener("mousedown", dismiss)
|
||||
document.removeEventListener("scroll", dismiss)
|
||||
|
@ -144,11 +201,15 @@ export function setUtilsListeners(manager: WindowManager) {
|
|||
}
|
||||
document.addEventListener("mousedown", dismiss)
|
||||
document.addEventListener("scroll", dismiss)
|
||||
})`).then(() => {
|
||||
if (manager.hasWindow()) {
|
||||
manager.mainWindow.webContents.send("webview-context-menu")
|
||||
}
|
||||
})
|
||||
})`
|
||||
)
|
||||
.then(() => {
|
||||
if (manager.hasWindow()) {
|
||||
manager.mainWindow.webContents.send(
|
||||
"webview-context-menu"
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
contents.on("before-input-event", (_, input) => {
|
||||
|
@ -176,23 +237,26 @@ export function setUtilsListeners(manager: WindowManager) {
|
|||
manager.zoom()
|
||||
})
|
||||
|
||||
ipcMain.on("is-maximized", (event) => {
|
||||
event.returnValue = Boolean(manager.mainWindow) && manager.mainWindow.isMaximized()
|
||||
ipcMain.on("is-maximized", event => {
|
||||
event.returnValue =
|
||||
Boolean(manager.mainWindow) && manager.mainWindow.isMaximized()
|
||||
})
|
||||
|
||||
ipcMain.on("is-focused", (event) => {
|
||||
event.returnValue = manager.hasWindow() && manager.mainWindow.isFocused()
|
||||
ipcMain.on("is-focused", event => {
|
||||
event.returnValue =
|
||||
manager.hasWindow() && manager.mainWindow.isFocused()
|
||||
})
|
||||
|
||||
ipcMain.on("is-fullscreen", (event) => {
|
||||
event.returnValue = manager.hasWindow() && manager.mainWindow.isFullScreen()
|
||||
ipcMain.on("is-fullscreen", event => {
|
||||
event.returnValue =
|
||||
manager.hasWindow() && manager.mainWindow.isFullScreen()
|
||||
})
|
||||
|
||||
ipcMain.handle("request-focus", () => {
|
||||
if (manager.hasWindow()) {
|
||||
const win = manager.mainWindow
|
||||
if (win.isMinimized()) win.restore()
|
||||
if (process.platform === "win32") {
|
||||
if (process.platform === "win32") {
|
||||
win.setAlwaysOnTop(true)
|
||||
win.setAlwaysOnTop(false)
|
||||
}
|
||||
|
@ -219,4 +283,4 @@ export function setUtilsListeners(manager: WindowManager) {
|
|||
ipcMain.handle("touchbar-destroy", () => {
|
||||
if (manager.hasWindow()) manager.mainWindow.setTouchBar(null)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ export class WindowManager {
|
|||
this.mainWindow.focus()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
app.on("activate", () => {
|
||||
if (this.mainWindow === null) {
|
||||
this.createWindow()
|
||||
|
@ -44,7 +44,12 @@ export class WindowManager {
|
|||
if (!this.hasWindow()) {
|
||||
this.mainWindow = new BrowserWindow({
|
||||
title: "Fluent Reader",
|
||||
backgroundColor: process.platform === "darwin" ? "#00000000" : (nativeTheme.shouldUseDarkColors ? "#282828" : "#faf9f8"),
|
||||
backgroundColor:
|
||||
process.platform === "darwin"
|
||||
? "#00000000"
|
||||
: nativeTheme.shouldUseDarkColors
|
||||
? "#282828"
|
||||
: "#faf9f8",
|
||||
vibrancy: "sidebar",
|
||||
x: this.mainWindowState.x,
|
||||
y: this.mainWindowState.y,
|
||||
|
@ -62,8 +67,11 @@ export class WindowManager {
|
|||
contextIsolation: true,
|
||||
worldSafeExecuteJavaScript: true,
|
||||
spellcheck: false,
|
||||
preload: path.join(app.getAppPath(), (app.isPackaged ? "dist/" : "") + "preload.js")
|
||||
}
|
||||
preload: path.join(
|
||||
app.getAppPath(),
|
||||
(app.isPackaged ? "dist/" : "") + "preload.js"
|
||||
),
|
||||
},
|
||||
})
|
||||
this.mainWindowState.manage(this.mainWindow)
|
||||
this.mainWindow.on("ready-to-show", () => {
|
||||
|
@ -71,7 +79,9 @@ export class WindowManager {
|
|||
this.mainWindow.focus()
|
||||
if (!app.isPackaged) this.mainWindow.webContents.openDevTools()
|
||||
})
|
||||
this.mainWindow.loadFile((app.isPackaged ? "dist/" : "") + "index.html", )
|
||||
this.mainWindow.loadFile(
|
||||
(app.isPackaged ? "dist/" : "") + "index.html"
|
||||
)
|
||||
|
||||
this.mainWindow.on("maximize", () => {
|
||||
this.mainWindow.webContents.send("maximized")
|
||||
|
@ -93,7 +103,11 @@ export class WindowManager {
|
|||
})
|
||||
this.mainWindow.webContents.on("context-menu", (_, params) => {
|
||||
if (params.selectionText) {
|
||||
this.mainWindow.webContents.send("window-context-menu", [params.x, params.y], params.selectionText)
|
||||
this.mainWindow.webContents.send(
|
||||
"window-context-menu",
|
||||
[params.x, params.y],
|
||||
params.selectionText
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -112,4 +126,4 @@ export class WindowManager {
|
|||
hasWindow = () => {
|
||||
return this.mainWindow !== null && !this.mainWindow.isDestroyed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,11 @@ export class SourceGroup {
|
|||
}
|
||||
|
||||
export const enum ViewType {
|
||||
Cards, List, Magazine, Compact, Customized
|
||||
Cards,
|
||||
List,
|
||||
Magazine,
|
||||
Compact,
|
||||
Customized,
|
||||
}
|
||||
|
||||
export const enum ViewConfigs {
|
||||
|
@ -29,21 +33,32 @@ export const enum ViewConfigs {
|
|||
}
|
||||
|
||||
export const enum ThemeSettings {
|
||||
Default = "system",
|
||||
Light = "light",
|
||||
Dark = "dark"
|
||||
Default = "system",
|
||||
Light = "light",
|
||||
Dark = "dark",
|
||||
}
|
||||
|
||||
export const enum SearchEngines {
|
||||
Google, Bing, Baidu, DuckDuckGo
|
||||
Google,
|
||||
Bing,
|
||||
Baidu,
|
||||
DuckDuckGo,
|
||||
}
|
||||
|
||||
export const enum ImageCallbackTypes {
|
||||
OpenExternal, OpenExternalBg, SaveAs, Copy, CopyLink
|
||||
OpenExternal,
|
||||
OpenExternalBg,
|
||||
SaveAs,
|
||||
Copy,
|
||||
CopyLink,
|
||||
}
|
||||
|
||||
export const enum SyncService {
|
||||
None, Fever, Feedbin, GReader, Inoreader
|
||||
None,
|
||||
Fever,
|
||||
Feedbin,
|
||||
GReader,
|
||||
Inoreader,
|
||||
}
|
||||
export interface ServiceConfigs {
|
||||
type: SyncService
|
||||
|
@ -51,7 +66,9 @@ export interface ServiceConfigs {
|
|||
}
|
||||
|
||||
export const enum WindowStateListenerType {
|
||||
Maximized, Focused, Fullscreen
|
||||
Maximized,
|
||||
Focused,
|
||||
Fullscreen,
|
||||
}
|
||||
|
||||
export interface TouchBarTexts {
|
||||
|
|
|
@ -5,39 +5,43 @@ import { RSSSource } from "./models/source"
|
|||
import { RSSItem } from "./models/item"
|
||||
|
||||
const sdbSchema = lf.schema.create("sourcesDB", 1)
|
||||
sdbSchema.createTable("sources").
|
||||
addColumn("sid", lf.Type.INTEGER).addPrimaryKey(["sid"], false).
|
||||
addColumn("url", lf.Type.STRING).
|
||||
addColumn("iconurl", lf.Type.STRING).
|
||||
addColumn("name", lf.Type.STRING).
|
||||
addColumn("openTarget", lf.Type.NUMBER).
|
||||
addColumn("lastFetched", lf.Type.DATE_TIME).
|
||||
addColumn("serviceRef", lf.Type.STRING).
|
||||
addColumn("fetchFrequency", lf.Type.NUMBER).
|
||||
addColumn("rules", lf.Type.OBJECT).
|
||||
addNullable(["iconurl", "serviceRef", "rules"]).
|
||||
addIndex("idxURL", ["url"], true)
|
||||
sdbSchema
|
||||
.createTable("sources")
|
||||
.addColumn("sid", lf.Type.INTEGER)
|
||||
.addPrimaryKey(["sid"], false)
|
||||
.addColumn("url", lf.Type.STRING)
|
||||
.addColumn("iconurl", lf.Type.STRING)
|
||||
.addColumn("name", lf.Type.STRING)
|
||||
.addColumn("openTarget", lf.Type.NUMBER)
|
||||
.addColumn("lastFetched", lf.Type.DATE_TIME)
|
||||
.addColumn("serviceRef", lf.Type.STRING)
|
||||
.addColumn("fetchFrequency", lf.Type.NUMBER)
|
||||
.addColumn("rules", lf.Type.OBJECT)
|
||||
.addNullable(["iconurl", "serviceRef", "rules"])
|
||||
.addIndex("idxURL", ["url"], true)
|
||||
|
||||
const idbSchema = lf.schema.create("itemsDB", 1)
|
||||
idbSchema.createTable("items").
|
||||
addColumn("_id", lf.Type.INTEGER).addPrimaryKey(["_id"], true).
|
||||
addColumn("source", lf.Type.INTEGER).
|
||||
addColumn("title", lf.Type.STRING).
|
||||
addColumn("link", lf.Type.STRING).
|
||||
addColumn("date", lf.Type.DATE_TIME).
|
||||
addColumn("fetchedDate", lf.Type.DATE_TIME).
|
||||
addColumn("thumb", lf.Type.STRING).
|
||||
addColumn("content", lf.Type.STRING).
|
||||
addColumn("snippet", lf.Type.STRING).
|
||||
addColumn("creator", lf.Type.STRING).
|
||||
addColumn("hasRead", lf.Type.BOOLEAN).
|
||||
addColumn("starred", lf.Type.BOOLEAN).
|
||||
addColumn("hidden", lf.Type.BOOLEAN).
|
||||
addColumn("notify", lf.Type.BOOLEAN).
|
||||
addColumn("serviceRef", lf.Type.STRING).
|
||||
addNullable(["thumb", "creator", "serviceRef"]).
|
||||
addIndex("idxDate", ["date"], false, lf.Order.DESC).
|
||||
addIndex("idxService", ["serviceRef"], false)
|
||||
idbSchema
|
||||
.createTable("items")
|
||||
.addColumn("_id", lf.Type.INTEGER)
|
||||
.addPrimaryKey(["_id"], true)
|
||||
.addColumn("source", lf.Type.INTEGER)
|
||||
.addColumn("title", lf.Type.STRING)
|
||||
.addColumn("link", lf.Type.STRING)
|
||||
.addColumn("date", lf.Type.DATE_TIME)
|
||||
.addColumn("fetchedDate", lf.Type.DATE_TIME)
|
||||
.addColumn("thumb", lf.Type.STRING)
|
||||
.addColumn("content", lf.Type.STRING)
|
||||
.addColumn("snippet", lf.Type.STRING)
|
||||
.addColumn("creator", lf.Type.STRING)
|
||||
.addColumn("hasRead", lf.Type.BOOLEAN)
|
||||
.addColumn("starred", lf.Type.BOOLEAN)
|
||||
.addColumn("hidden", lf.Type.BOOLEAN)
|
||||
.addColumn("notify", lf.Type.BOOLEAN)
|
||||
.addColumn("serviceRef", lf.Type.STRING)
|
||||
.addNullable(["thumb", "creator", "serviceRef"])
|
||||
.addIndex("idxDate", ["date"], false, lf.Order.DESC)
|
||||
.addIndex("idxService", ["serviceRef"], false)
|
||||
|
||||
export let sourcesDB: lf.Database
|
||||
export let sources: lf.schema.Table
|
||||
|
@ -59,16 +63,16 @@ async function migrateNeDB() {
|
|||
const sdb = new Datastore<RSSSource>({
|
||||
filename: "sources",
|
||||
autoload: true,
|
||||
onload: (err) => {
|
||||
onload: err => {
|
||||
if (err) window.console.log(err)
|
||||
}
|
||||
},
|
||||
})
|
||||
const idb = new Datastore<RSSItem>({
|
||||
filename: "items",
|
||||
autoload: true,
|
||||
onload: (err) => {
|
||||
onload: err => {
|
||||
if (err) window.console.log(err)
|
||||
}
|
||||
},
|
||||
})
|
||||
const sourceDocs = await new Promise<RSSSource[]>(resolve => {
|
||||
sdb.find({}, (_, docs) => {
|
||||
|
@ -81,14 +85,16 @@ async function migrateNeDB() {
|
|||
})
|
||||
})
|
||||
const sRows = sourceDocs.map(doc => {
|
||||
if (doc.serviceRef !== undefined) doc.serviceRef = String(doc.serviceRef)
|
||||
if (doc.serviceRef !== undefined)
|
||||
doc.serviceRef = String(doc.serviceRef)
|
||||
// @ts-ignore
|
||||
delete doc._id
|
||||
if (!doc.fetchFrequency) doc.fetchFrequency = 0
|
||||
return sources.createRow(doc)
|
||||
})
|
||||
const iRows = itemDocs.map(doc => {
|
||||
if (doc.serviceRef !== undefined) doc.serviceRef = String(doc.serviceRef)
|
||||
if (doc.serviceRef !== undefined)
|
||||
doc.serviceRef = String(doc.serviceRef)
|
||||
if (!doc.title) doc.title = intl.get("article.untitled")
|
||||
if (!doc.content) doc.content = ""
|
||||
if (!doc.snippet) doc.snippet = ""
|
||||
|
@ -100,13 +106,20 @@ async function migrateNeDB() {
|
|||
})
|
||||
await Promise.all([
|
||||
sourcesDB.insert().into(sources).values(sRows).exec(),
|
||||
itemsDB.insert().into(items).values(iRows).exec()
|
||||
itemsDB.insert().into(items).values(iRows).exec(),
|
||||
])
|
||||
window.settings.setNeDBStatus(false)
|
||||
sdb.remove({}, { multi: true }, () => { sdb.persistence.compactDatafile() })
|
||||
idb.remove({}, { multi: true }, () => { idb.persistence.compactDatafile() })
|
||||
sdb.remove({}, { multi: true }, () => {
|
||||
sdb.persistence.compactDatafile()
|
||||
})
|
||||
idb.remove({}, { multi: true }, () => {
|
||||
idb.persistence.compactDatafile()
|
||||
})
|
||||
} catch (err) {
|
||||
window.utils.showErrorBox("An error has occured during update. Please report this error on GitHub.", String(err))
|
||||
window.utils.showErrorBox(
|
||||
"An error has occured during update. Please report this error on GitHub.",
|
||||
String(err)
|
||||
)
|
||||
window.utils.closeWindow()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -224,4 +224,4 @@
|
|||
"fetchInterval": "Interval zwischen dem Abrufen der Daten",
|
||||
"never": "Nie"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -232,4 +232,4 @@
|
|||
"fetchInterval": "Automatic fetch interval",
|
||||
"never": "Never"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,235 +1,235 @@
|
|||
{
|
||||
"add": "Lisää",
|
||||
"allArticles": "Kaikki artikkelit",
|
||||
"app": {
|
||||
"backup": "Varmuuskopiointi",
|
||||
"badUrl": "Virheellinen URL",
|
||||
"cache": "Tyhjennä välimuisti",
|
||||
"cacheSize": "Välimuistissa on {size} dataa",
|
||||
"calculatingSize": "Lasketaan kokoa...",
|
||||
"cleanup": "Siivoa",
|
||||
"confirmDelete": "Poista",
|
||||
"confirmImport": "Haluatko tuoda tiedot varmuuskopiosta? Kaikki nykyinen data poistetaan.",
|
||||
"darkTheme": "Tumma tila",
|
||||
"data": "Sovelluksen Data",
|
||||
"daysAgo": "{days, plural, =1 {# päivä} other {# päivää}} sitten",
|
||||
"deleteAll": "Poista kaikki artikkelit",
|
||||
"deleteChoices": "Poista ... päivää vanhemmat artikkelit",
|
||||
"enableProxy": "Käytä välityspalvelinta",
|
||||
"fetchInterval": "Automaattinen päivitys",
|
||||
"frData": "Fluent Reader Data",
|
||||
"itemSize": "Artikkelit käyttävät {size} paikallista tallennustilaa",
|
||||
"language": "Näyttökieli",
|
||||
"lightTheme": "Vaalea tila",
|
||||
"never": "Ei koskaan",
|
||||
"pac": "PAC Osoite",
|
||||
"pacHint": "Socks -välityspalvelimille on suositeltavaa asettaa PAC palauttamaan \"SOCKS5\" välityspalvelminen DNS:tä. Välityspalvelimen laittaminen pois päältä vaatii uudelleenkäynnistyksen,",
|
||||
"restore": "Palauta",
|
||||
"setPac": "Aseta PAC",
|
||||
"theme": "Teema"
|
||||
},
|
||||
"article": {
|
||||
"dontNotify": "Älä ilmoita",
|
||||
"empty": "Ei artikkeleita",
|
||||
"error": "Artikkelin lataaminen epäonnistui.",
|
||||
"fontSize": "Fonttikoko",
|
||||
"hide": "Piilota artikkeli",
|
||||
"loadFull": "Lataa koko sisältö",
|
||||
"loadWebpage": "Lataa verkkosivu",
|
||||
"markAbove": "Merkitse ylemmät luetuksi",
|
||||
"markBelow": "Merkitse alemmat luetuksi",
|
||||
"markRead": "Merkitse luetuksi",
|
||||
"markUnread": "Merkitse lukemattomaksi",
|
||||
"notify": "Ilmoita, jos haetaan taustalla",
|
||||
"reload": "Ladataanko uudelleen?",
|
||||
"star": "Tähti",
|
||||
"unhide": "Näytä artikkeli",
|
||||
"unstar": "Poista tähti",
|
||||
"untitled": "(Nimetön)"
|
||||
},
|
||||
"cancel": "Peruuta",
|
||||
"close": "Sulje",
|
||||
"confirm": "Vahvista",
|
||||
"confirmMarkAll": "Haluatko todella merkitä kaikki tämän sivun artikkelit luetuiksi?",
|
||||
"context": {
|
||||
"cardView": "Kortit",
|
||||
"caseSensitive": "Vain sama merkkikoko",
|
||||
"compactView": "Kompakti",
|
||||
"copy": "Kopioi",
|
||||
"copyImage": "Kopioi kuva",
|
||||
"copyImageURL": "Kopioi kuvalinkki",
|
||||
"copyTitle": "Kopioi otsikko",
|
||||
"copyURL": "Kopioi linkki",
|
||||
"fadeRead": "Häivytä luetut artikkelit",
|
||||
"filter": "Suodatus",
|
||||
"fullSearch": "Hae koko tekstistä",
|
||||
"listView": "Lista",
|
||||
"magazineView": "Lehti",
|
||||
"manageSources": "Hallitse lähteitä",
|
||||
"read": "Luettu",
|
||||
"saveImageAs": "Tallenna kuva nimellä…",
|
||||
"search": "Hae \"{text}\" {engine}lla",
|
||||
"share": "Jaa",
|
||||
"showCover": "Näytä artikkelin kuva",
|
||||
"showHidden": "Näytä piilotetut artikkelit",
|
||||
"showSnippet": "Näytä katkelma",
|
||||
"starredOnly": "Vain tähdellä merkityt",
|
||||
"unreadOnly": "Vain lukemattomat",
|
||||
"view": "Näkymä"
|
||||
},
|
||||
"create": "Luo",
|
||||
"dangerButton": "Vahvistetaanko {action}?",
|
||||
"delete": "Poista",
|
||||
"edit": "Muokkaa",
|
||||
"emptyField": "Tämä kenttä ei voi olla tyhjä.",
|
||||
"emptyName": "Tämä kenttä ei voi olla tyhjä.",
|
||||
"followSystem": "Käytä järjestelmän teemaa",
|
||||
"groups": {
|
||||
"addToGroup": "Lisää ...",
|
||||
"capacity": "Määrä",
|
||||
"chooseGroup": "Valitse ryhmä",
|
||||
"create": "Luo ryhmä",
|
||||
"deleteGroup": "Poista ryhmä",
|
||||
"deleteSource": "Poista ryhmästä",
|
||||
"editName": "Muokkaa nimeä",
|
||||
"enterName": "Anna nimi",
|
||||
"exist": "Tämä ryhmä on jo olemassa.",
|
||||
"exitGroup": "Takaisin ryhmiin",
|
||||
"group": "Ryhmä",
|
||||
"groupHint": "Kaksoisnapsauta ryhmää muokataksesi sen lähteitä. Järjestä uudelleen vetämällä ja pudottamalla.",
|
||||
"selectedGroup": "Valittu ryhmä",
|
||||
"selectedSource": "Valittu lähde",
|
||||
"source": "Lähde",
|
||||
"sourceHint": "Järjestä uudelleen vetämällä ja pudottamalla lähteitä.",
|
||||
"type": "Tyyppi"
|
||||
},
|
||||
"icon": "Kuvake",
|
||||
"loadMore": "Lataa lisää",
|
||||
"log": {
|
||||
"empty": "Ei ilmoituksia",
|
||||
"fetchFailure": "Lähteen \"{name}\" lataaminen epäonnistui.",
|
||||
"fetchSuccess": "Noudettiin onnistuneesti {count, plural, =1 {# artikkeli} other {# artikkelia}}.",
|
||||
"networkError": "Tapahtui verkkovirhe.",
|
||||
"parseError": "XML-syötteen jäsentämisessä tapahtui virhe.",
|
||||
"syncFailure": "Palvelun kanssa synkronointi epäonnistui"
|
||||
},
|
||||
"menu": {
|
||||
"close": "Sulje valikko",
|
||||
"subscriptions": "Tilaukset"
|
||||
},
|
||||
"more": "Lisää",
|
||||
"name": "Nimi",
|
||||
"nav": {
|
||||
"markAllRead": "Merkitse kaikki luetuksi",
|
||||
"maximize": "Laajenna",
|
||||
"menu": "Valikko",
|
||||
"minimize": "Pienennä",
|
||||
"notifications": "Ilmoitukset",
|
||||
"refresh": "Päivitä",
|
||||
"settings": "Asetukset",
|
||||
"view": "Näkymä"
|
||||
},
|
||||
"openExternal": "Avaa selaimessa",
|
||||
"rules": {
|
||||
"action": "Toiminto",
|
||||
"badRegex": "Virheellinen sääntö.",
|
||||
"content": "Sisältö",
|
||||
"creator": "Kirjoittaja",
|
||||
"fullSearch": "Otsikko tai sisältö",
|
||||
"help": "Lisätietoja",
|
||||
"hint": "Sääntöjä sovelletaan järjestyksessä. Järjestä uudelleen vetämällä ja pudottamalla.",
|
||||
"if": "Jos",
|
||||
"intro": "Merkitse artikkelit automaattisesti tai lähetä ilmoituksia säännöllisin lausekkein.",
|
||||
"match": "täsmää",
|
||||
"new": "Uusi sääntö",
|
||||
"notMatch": "ei täsmää",
|
||||
"regex": "Säännöllinen lauseke",
|
||||
"selectAction": "Valitse toiminnot",
|
||||
"selectSource": "Valitse lähde",
|
||||
"source": "Lähde",
|
||||
"test": "Testaa sääntöä",
|
||||
"then": "Sitten",
|
||||
"title": "Otsikko"
|
||||
},
|
||||
"search": "Hae",
|
||||
"searchEngine": {
|
||||
"baidu": "Baidu",
|
||||
"bing": "Bing",
|
||||
"duckduckgo": "DuckDuckGo",
|
||||
"google": "Google",
|
||||
"name": "Hakukone"
|
||||
},
|
||||
"service": {
|
||||
"endpoint": "Osoite",
|
||||
"exportToLite": "Vie Fluent Reader Lite -ohjelmaan",
|
||||
"failure": "Palveluun ei voi muodostaa yhteyttä",
|
||||
"failureHint": "Tarkista palvelun asetukset tai verkkoyhteytesi.",
|
||||
"fetchLimit": "Synkronointiraja",
|
||||
"fetchLimitNum": "{count} viimeisintä artikkelia",
|
||||
"fetchUnlimited": "Rajoittamaton (ei suositella)",
|
||||
"groupsWarning": "Ryhmiä ei synkronoida automaattisesti palvelun kanssa.",
|
||||
"importGroups": "Tuo ryhmät",
|
||||
"intro": "Synkronoi laitteiden välillä RSS-palveluiden kanssa.",
|
||||
"overwriteWarning": "Paikalliset lähteet poistetaan, jos niitä on palvelussa.",
|
||||
"password": "Salasana",
|
||||
"rateLimitWarning": "Rajapintakäytön rajoituksien välttämiseksi sinun on luotava oma API-avain.",
|
||||
"removeAd": "Poista mainos",
|
||||
"select": "Valitse palvelu",
|
||||
"suggest": "Ehdota uutta palvelua",
|
||||
"unchanged": "Ei muutoksia",
|
||||
"username": "Käyttäjätunnus"
|
||||
},
|
||||
"settings": {
|
||||
"about": "Tietoja",
|
||||
"app": "Asetukset",
|
||||
"exit": "Poistu asetuksista",
|
||||
"feedback": "Palaute",
|
||||
"fetching": "Päivitetään lähteitä, odota…",
|
||||
"grouping": "Ryhmät",
|
||||
"name": "Asetukset",
|
||||
"openSource": "Avoin lähdekoodi",
|
||||
"rules": "Säännöt",
|
||||
"service": "Palvelu",
|
||||
"shortcuts": "Pikakomennot",
|
||||
"sources": "Lähteet",
|
||||
"version": "Versio",
|
||||
"writeError": "Tiedostoa kirjoitettaessa tapahtui virhe."
|
||||
},
|
||||
"sources": {
|
||||
"add": "Lisää lähde",
|
||||
"badIcon": "Virheellinen kuvake",
|
||||
"badUrl": "Virheellinen URL",
|
||||
"delete": "Poista lähde",
|
||||
"deleteWarning": "Lähde ja kaikki tallennetut artikkelit poistetaan.",
|
||||
"editName": "Muokkaa nimeä",
|
||||
"errorAdd": "Lähdettä lisättäessä tapahtui virhe.",
|
||||
"errorImport": "Virhe tuotaessa {count, plural, =1 {# lähdettä} other {# lähteitä}}.",
|
||||
"errorParse": "OPML -tiedoston jäsentämisessä tapahtui virhe.",
|
||||
"errorParseHint": "Varmista, että tiedosto ei ole vioittunut ja että se on koodattu UTF-8: lla.",
|
||||
"exist": "Tämä lähde on jo olemassa.",
|
||||
"export": "Vie",
|
||||
"fetchFrequency": "Tietojen hakemisen raja",
|
||||
"import": "Tuo",
|
||||
"inputUrl": "Syötä URL",
|
||||
"loadWebpage": "Lataa verkkosivu",
|
||||
"name": "Lähteen nimi",
|
||||
"openTarget": "Avaa artikkelit oletuksena",
|
||||
"opmlFile": "OPML -tiedosto",
|
||||
"rssText": "RSS koko teksti",
|
||||
"selected": "Valittu lähde",
|
||||
"selectedMulti": "Valittu useita lähteitä",
|
||||
"serviceManaged": "Palvelu hallinnoi tätä lähdettä.",
|
||||
"serviceWarning": "Täältä tuotuja tai lisättyjä lähteitä ei synkronoida palvelusi kanssa.",
|
||||
"unlimited": "Rajoittamaton",
|
||||
"untitled": "Lähde"
|
||||
},
|
||||
"time": {
|
||||
"d": "pv",
|
||||
"day": "{d, plural, =1 {# päivä} other {# päivää}}",
|
||||
"h": "h",
|
||||
"hour": "{h, plural, =1 {# tunti} other {# tuntia}}",
|
||||
"m": "min",
|
||||
"minute": "{m, plural, =1 {# minutti} other {# minuttia}}",
|
||||
"now": "nyt"
|
||||
}
|
||||
}
|
||||
{
|
||||
"add": "Lisää",
|
||||
"allArticles": "Kaikki artikkelit",
|
||||
"app": {
|
||||
"backup": "Varmuuskopiointi",
|
||||
"badUrl": "Virheellinen URL",
|
||||
"cache": "Tyhjennä välimuisti",
|
||||
"cacheSize": "Välimuistissa on {size} dataa",
|
||||
"calculatingSize": "Lasketaan kokoa...",
|
||||
"cleanup": "Siivoa",
|
||||
"confirmDelete": "Poista",
|
||||
"confirmImport": "Haluatko tuoda tiedot varmuuskopiosta? Kaikki nykyinen data poistetaan.",
|
||||
"darkTheme": "Tumma tila",
|
||||
"data": "Sovelluksen Data",
|
||||
"daysAgo": "{days, plural, =1 {# päivä} other {# päivää}} sitten",
|
||||
"deleteAll": "Poista kaikki artikkelit",
|
||||
"deleteChoices": "Poista ... päivää vanhemmat artikkelit",
|
||||
"enableProxy": "Käytä välityspalvelinta",
|
||||
"fetchInterval": "Automaattinen päivitys",
|
||||
"frData": "Fluent Reader Data",
|
||||
"itemSize": "Artikkelit käyttävät {size} paikallista tallennustilaa",
|
||||
"language": "Näyttökieli",
|
||||
"lightTheme": "Vaalea tila",
|
||||
"never": "Ei koskaan",
|
||||
"pac": "PAC Osoite",
|
||||
"pacHint": "Socks -välityspalvelimille on suositeltavaa asettaa PAC palauttamaan \"SOCKS5\" välityspalvelminen DNS:tä. Välityspalvelimen laittaminen pois päältä vaatii uudelleenkäynnistyksen,",
|
||||
"restore": "Palauta",
|
||||
"setPac": "Aseta PAC",
|
||||
"theme": "Teema"
|
||||
},
|
||||
"article": {
|
||||
"dontNotify": "Älä ilmoita",
|
||||
"empty": "Ei artikkeleita",
|
||||
"error": "Artikkelin lataaminen epäonnistui.",
|
||||
"fontSize": "Fonttikoko",
|
||||
"hide": "Piilota artikkeli",
|
||||
"loadFull": "Lataa koko sisältö",
|
||||
"loadWebpage": "Lataa verkkosivu",
|
||||
"markAbove": "Merkitse ylemmät luetuksi",
|
||||
"markBelow": "Merkitse alemmat luetuksi",
|
||||
"markRead": "Merkitse luetuksi",
|
||||
"markUnread": "Merkitse lukemattomaksi",
|
||||
"notify": "Ilmoita, jos haetaan taustalla",
|
||||
"reload": "Ladataanko uudelleen?",
|
||||
"star": "Tähti",
|
||||
"unhide": "Näytä artikkeli",
|
||||
"unstar": "Poista tähti",
|
||||
"untitled": "(Nimetön)"
|
||||
},
|
||||
"cancel": "Peruuta",
|
||||
"close": "Sulje",
|
||||
"confirm": "Vahvista",
|
||||
"confirmMarkAll": "Haluatko todella merkitä kaikki tämän sivun artikkelit luetuiksi?",
|
||||
"context": {
|
||||
"cardView": "Kortit",
|
||||
"caseSensitive": "Vain sama merkkikoko",
|
||||
"compactView": "Kompakti",
|
||||
"copy": "Kopioi",
|
||||
"copyImage": "Kopioi kuva",
|
||||
"copyImageURL": "Kopioi kuvalinkki",
|
||||
"copyTitle": "Kopioi otsikko",
|
||||
"copyURL": "Kopioi linkki",
|
||||
"fadeRead": "Häivytä luetut artikkelit",
|
||||
"filter": "Suodatus",
|
||||
"fullSearch": "Hae koko tekstistä",
|
||||
"listView": "Lista",
|
||||
"magazineView": "Lehti",
|
||||
"manageSources": "Hallitse lähteitä",
|
||||
"read": "Luettu",
|
||||
"saveImageAs": "Tallenna kuva nimellä…",
|
||||
"search": "Hae \"{text}\" {engine}lla",
|
||||
"share": "Jaa",
|
||||
"showCover": "Näytä artikkelin kuva",
|
||||
"showHidden": "Näytä piilotetut artikkelit",
|
||||
"showSnippet": "Näytä katkelma",
|
||||
"starredOnly": "Vain tähdellä merkityt",
|
||||
"unreadOnly": "Vain lukemattomat",
|
||||
"view": "Näkymä"
|
||||
},
|
||||
"create": "Luo",
|
||||
"dangerButton": "Vahvistetaanko {action}?",
|
||||
"delete": "Poista",
|
||||
"edit": "Muokkaa",
|
||||
"emptyField": "Tämä kenttä ei voi olla tyhjä.",
|
||||
"emptyName": "Tämä kenttä ei voi olla tyhjä.",
|
||||
"followSystem": "Käytä järjestelmän teemaa",
|
||||
"groups": {
|
||||
"addToGroup": "Lisää ...",
|
||||
"capacity": "Määrä",
|
||||
"chooseGroup": "Valitse ryhmä",
|
||||
"create": "Luo ryhmä",
|
||||
"deleteGroup": "Poista ryhmä",
|
||||
"deleteSource": "Poista ryhmästä",
|
||||
"editName": "Muokkaa nimeä",
|
||||
"enterName": "Anna nimi",
|
||||
"exist": "Tämä ryhmä on jo olemassa.",
|
||||
"exitGroup": "Takaisin ryhmiin",
|
||||
"group": "Ryhmä",
|
||||
"groupHint": "Kaksoisnapsauta ryhmää muokataksesi sen lähteitä. Järjestä uudelleen vetämällä ja pudottamalla.",
|
||||
"selectedGroup": "Valittu ryhmä",
|
||||
"selectedSource": "Valittu lähde",
|
||||
"source": "Lähde",
|
||||
"sourceHint": "Järjestä uudelleen vetämällä ja pudottamalla lähteitä.",
|
||||
"type": "Tyyppi"
|
||||
},
|
||||
"icon": "Kuvake",
|
||||
"loadMore": "Lataa lisää",
|
||||
"log": {
|
||||
"empty": "Ei ilmoituksia",
|
||||
"fetchFailure": "Lähteen \"{name}\" lataaminen epäonnistui.",
|
||||
"fetchSuccess": "Noudettiin onnistuneesti {count, plural, =1 {# artikkeli} other {# artikkelia}}.",
|
||||
"networkError": "Tapahtui verkkovirhe.",
|
||||
"parseError": "XML-syötteen jäsentämisessä tapahtui virhe.",
|
||||
"syncFailure": "Palvelun kanssa synkronointi epäonnistui"
|
||||
},
|
||||
"menu": {
|
||||
"close": "Sulje valikko",
|
||||
"subscriptions": "Tilaukset"
|
||||
},
|
||||
"more": "Lisää",
|
||||
"name": "Nimi",
|
||||
"nav": {
|
||||
"markAllRead": "Merkitse kaikki luetuksi",
|
||||
"maximize": "Laajenna",
|
||||
"menu": "Valikko",
|
||||
"minimize": "Pienennä",
|
||||
"notifications": "Ilmoitukset",
|
||||
"refresh": "Päivitä",
|
||||
"settings": "Asetukset",
|
||||
"view": "Näkymä"
|
||||
},
|
||||
"openExternal": "Avaa selaimessa",
|
||||
"rules": {
|
||||
"action": "Toiminto",
|
||||
"badRegex": "Virheellinen sääntö.",
|
||||
"content": "Sisältö",
|
||||
"creator": "Kirjoittaja",
|
||||
"fullSearch": "Otsikko tai sisältö",
|
||||
"help": "Lisätietoja",
|
||||
"hint": "Sääntöjä sovelletaan järjestyksessä. Järjestä uudelleen vetämällä ja pudottamalla.",
|
||||
"if": "Jos",
|
||||
"intro": "Merkitse artikkelit automaattisesti tai lähetä ilmoituksia säännöllisin lausekkein.",
|
||||
"match": "täsmää",
|
||||
"new": "Uusi sääntö",
|
||||
"notMatch": "ei täsmää",
|
||||
"regex": "Säännöllinen lauseke",
|
||||
"selectAction": "Valitse toiminnot",
|
||||
"selectSource": "Valitse lähde",
|
||||
"source": "Lähde",
|
||||
"test": "Testaa sääntöä",
|
||||
"then": "Sitten",
|
||||
"title": "Otsikko"
|
||||
},
|
||||
"search": "Hae",
|
||||
"searchEngine": {
|
||||
"baidu": "Baidu",
|
||||
"bing": "Bing",
|
||||
"duckduckgo": "DuckDuckGo",
|
||||
"google": "Google",
|
||||
"name": "Hakukone"
|
||||
},
|
||||
"service": {
|
||||
"endpoint": "Osoite",
|
||||
"exportToLite": "Vie Fluent Reader Lite -ohjelmaan",
|
||||
"failure": "Palveluun ei voi muodostaa yhteyttä",
|
||||
"failureHint": "Tarkista palvelun asetukset tai verkkoyhteytesi.",
|
||||
"fetchLimit": "Synkronointiraja",
|
||||
"fetchLimitNum": "{count} viimeisintä artikkelia",
|
||||
"fetchUnlimited": "Rajoittamaton (ei suositella)",
|
||||
"groupsWarning": "Ryhmiä ei synkronoida automaattisesti palvelun kanssa.",
|
||||
"importGroups": "Tuo ryhmät",
|
||||
"intro": "Synkronoi laitteiden välillä RSS-palveluiden kanssa.",
|
||||
"overwriteWarning": "Paikalliset lähteet poistetaan, jos niitä on palvelussa.",
|
||||
"password": "Salasana",
|
||||
"rateLimitWarning": "Rajapintakäytön rajoituksien välttämiseksi sinun on luotava oma API-avain.",
|
||||
"removeAd": "Poista mainos",
|
||||
"select": "Valitse palvelu",
|
||||
"suggest": "Ehdota uutta palvelua",
|
||||
"unchanged": "Ei muutoksia",
|
||||
"username": "Käyttäjätunnus"
|
||||
},
|
||||
"settings": {
|
||||
"about": "Tietoja",
|
||||
"app": "Asetukset",
|
||||
"exit": "Poistu asetuksista",
|
||||
"feedback": "Palaute",
|
||||
"fetching": "Päivitetään lähteitä, odota…",
|
||||
"grouping": "Ryhmät",
|
||||
"name": "Asetukset",
|
||||
"openSource": "Avoin lähdekoodi",
|
||||
"rules": "Säännöt",
|
||||
"service": "Palvelu",
|
||||
"shortcuts": "Pikakomennot",
|
||||
"sources": "Lähteet",
|
||||
"version": "Versio",
|
||||
"writeError": "Tiedostoa kirjoitettaessa tapahtui virhe."
|
||||
},
|
||||
"sources": {
|
||||
"add": "Lisää lähde",
|
||||
"badIcon": "Virheellinen kuvake",
|
||||
"badUrl": "Virheellinen URL",
|
||||
"delete": "Poista lähde",
|
||||
"deleteWarning": "Lähde ja kaikki tallennetut artikkelit poistetaan.",
|
||||
"editName": "Muokkaa nimeä",
|
||||
"errorAdd": "Lähdettä lisättäessä tapahtui virhe.",
|
||||
"errorImport": "Virhe tuotaessa {count, plural, =1 {# lähdettä} other {# lähteitä}}.",
|
||||
"errorParse": "OPML -tiedoston jäsentämisessä tapahtui virhe.",
|
||||
"errorParseHint": "Varmista, että tiedosto ei ole vioittunut ja että se on koodattu UTF-8: lla.",
|
||||
"exist": "Tämä lähde on jo olemassa.",
|
||||
"export": "Vie",
|
||||
"fetchFrequency": "Tietojen hakemisen raja",
|
||||
"import": "Tuo",
|
||||
"inputUrl": "Syötä URL",
|
||||
"loadWebpage": "Lataa verkkosivu",
|
||||
"name": "Lähteen nimi",
|
||||
"openTarget": "Avaa artikkelit oletuksena",
|
||||
"opmlFile": "OPML -tiedosto",
|
||||
"rssText": "RSS koko teksti",
|
||||
"selected": "Valittu lähde",
|
||||
"selectedMulti": "Valittu useita lähteitä",
|
||||
"serviceManaged": "Palvelu hallinnoi tätä lähdettä.",
|
||||
"serviceWarning": "Täältä tuotuja tai lisättyjä lähteitä ei synkronoida palvelusi kanssa.",
|
||||
"unlimited": "Rajoittamaton",
|
||||
"untitled": "Lähde"
|
||||
},
|
||||
"time": {
|
||||
"d": "pv",
|
||||
"day": "{d, plural, =1 {# päivä} other {# päivää}}",
|
||||
"h": "h",
|
||||
"hour": "{h, plural, =1 {# tunti} other {# tuntia}}",
|
||||
"m": "min",
|
||||
"minute": "{m, plural, =1 {# minutti} other {# minuttia}}",
|
||||
"now": "nyt"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -125,7 +125,7 @@
|
|||
"errorParse": "Une erreur s'est produite lors de l'analyse du fichier OPML.",
|
||||
"errorParseHint": "Veuillez vous assurer que le fichier n'est pas corrompu et qu'il est encodé en UTF-8.",
|
||||
"errorImport": "Erreur d'importation pour {count, plural, =1 {# source} other {# sources}}.",
|
||||
"exist": "Cette source existe déjà.",
|
||||
"exist": "Cette source existe déjà.",
|
||||
"opmlFile": "Fichier OPML",
|
||||
"name": "Nom de la source",
|
||||
"editName": "Modifier le nom",
|
||||
|
@ -229,4 +229,4 @@
|
|||
"fetchInterval": "Intervalle de récupération automatique",
|
||||
"never": "Jamais"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,236 +1,235 @@
|
|||
|
||||
{
|
||||
"allArticles": "Tutti gli articoli",
|
||||
"add": "Aggiungi",
|
||||
"create": "Crea",
|
||||
"icon": "Icona",
|
||||
"name": "Nome",
|
||||
"openExternal": "Apri Esternamente",
|
||||
"emptyName": "Questo campo non puo essere vuoto",
|
||||
"emptyField": "Questo campo non puo essere vuoto",
|
||||
"edit": "Modifica",
|
||||
"delete": "Elimina",
|
||||
"followSystem": "segui impostazioni di sistema",
|
||||
"more": "di più",
|
||||
"close": "Chiudi",
|
||||
"search": "Cerca",
|
||||
"loadMore": "Carica piu feed",
|
||||
"dangerButton": "Confermi di {action}?",
|
||||
"confirmMarkAll": "Vuoi veramente segnare tutti i feed di questa pagina come letti?",
|
||||
"confirm": "Confema",
|
||||
"cancel": "Anulla",
|
||||
"time": {
|
||||
"now": "ora",
|
||||
"m": "m",
|
||||
"h": "h",
|
||||
"d": "g",
|
||||
"minute": "{m, plural, =1 {# minuto} other {# minuti}}",
|
||||
"hour": "{h, plural, =1 {# ora} other {# ore}}",
|
||||
"day": "{d, plural, =1 {# giorno} other {# giorni}}"
|
||||
},
|
||||
"log": {
|
||||
"empty": "Non ci sono notifiche",
|
||||
"fetchFailure": "Errore nel caricare la fonte \"{name}\".",
|
||||
"fetchSuccess": "{count, plural, =1 {# articolo} other {# articoli}} caricato con successo",
|
||||
"networkError": "è occorso un errore di rete",
|
||||
"parseError": "è occorso un errore nel anallizzare il feed rss",
|
||||
"syncFailure": "Errore nel sicronizzarsi con il servizio"
|
||||
},
|
||||
"nav": {
|
||||
"menu": "Menu",
|
||||
"refresh": "Aggiorna",
|
||||
"markAllRead": "Segna tutti come letti",
|
||||
"notifications": "Notifiche",
|
||||
"view": "View",
|
||||
"settings": "Impostazioni",
|
||||
"minimize": "Riduci a Icona",
|
||||
"maximize": "Ingrandisci"
|
||||
},
|
||||
"menu": {
|
||||
"close": "Chiudi menu",
|
||||
"subscriptions": "Iscrizioni"
|
||||
},
|
||||
"article": {
|
||||
"error": "Errore nel caricare articolo",
|
||||
"reload": "Aggiorna?",
|
||||
"empty": "Non ci sono articoli",
|
||||
"untitled": "(Senza titolo)",
|
||||
"hide": "Nascondi articolo",
|
||||
"unhide": "Mostra articolo",
|
||||
"markRead": "Segna come letto",
|
||||
"markUnread": "Segna come non letto",
|
||||
"markAbove": "Segna precedenti come letti",
|
||||
"markBelow": "Segna successivi come letti",
|
||||
"star": "salva",
|
||||
"unstar": "rimuovi dai salvati",
|
||||
"fontSize": "Dimensione testo",
|
||||
"loadWebpage": "Carica pagina",
|
||||
"loadFull": "Carica tutto il contenuto",
|
||||
"notify": "Notifica se caricato in background",
|
||||
"dontNotify": "Non notificare"
|
||||
},
|
||||
"context": {
|
||||
"share": "Convidi",
|
||||
"read": "Leggi",
|
||||
"copyTitle": "Copia titolo",
|
||||
"copyURL": "Copia link",
|
||||
"copy": "Copia",
|
||||
"search": "Cerca \"{text}\" on {engine}",
|
||||
"view": "Visualizza",
|
||||
"cardView": "Card view",
|
||||
"listView": "List view",
|
||||
"magazineView": "Magazine view",
|
||||
"compactView": "Compact view",
|
||||
"filter": "Filtra",
|
||||
"unreadOnly": "Solo non letti",
|
||||
"starredOnly": "Solo Salvati",
|
||||
"fullSearch": "Cerca in tutto il lesto",
|
||||
"showHidden": "Visualizza articoli nascosti",
|
||||
"manageSources": "Gestisci fonti",
|
||||
"saveImageAs": "Salva immagine come …",
|
||||
"copyImage": "Copia immagine",
|
||||
"copyImageURL": "Copia immagine link",
|
||||
"caseSensitive": "Case sensitive",
|
||||
"showCover": "Mostra copertina",
|
||||
"showSnippet": "Show snippet",
|
||||
"fadeRead": "Scolorisci articoli letti"
|
||||
},
|
||||
"searchEngine": {
|
||||
"name": "Motore di ricerca",
|
||||
"google": "Google",
|
||||
"bing": "Bing",
|
||||
"baidu": "Baidu",
|
||||
"duckduckgo": "DuckDuckGo"
|
||||
},
|
||||
"settings": {
|
||||
"writeError": "è occorso un errore nello scrivere il file",
|
||||
"name": "Impostazioni",
|
||||
"fetching": "Aggiornamento delle fonti..Attendi",
|
||||
"exit": "Esci dalle Impostazioni",
|
||||
"sources": "Fonti",
|
||||
"grouping": "Gruppi",
|
||||
"rules": "Regole",
|
||||
"service": "Servizio",
|
||||
"app": "Preferenze",
|
||||
"about": "Informazioni",
|
||||
"version": "Versione",
|
||||
"shortcuts": "Shortcuts",
|
||||
"openSource": "Open source",
|
||||
"feedback": "Feedback"
|
||||
},
|
||||
"sources": {
|
||||
"serviceWarning": "le fonti importate o aggiunte qui non saranno sincronizzate con il tuo servizio",
|
||||
"serviceManaged": "la fonte è gesita dal tuo servizio",
|
||||
"untitled": "Fonte",
|
||||
"errorAdd": "Un errore è occorso nel caricare la fonte",
|
||||
"errorParse": "Un errore è occorso nell analizzare il file OPML",
|
||||
"errorParseInt":"Assicurati che il file non sia corrotto è sia in formato utf-8",
|
||||
"errorImport": "Errore nel importare {count, plural, =1 {# fonte} other {# fonti}}.",
|
||||
"exist": "Questa fonte è gia stata aggiunta",
|
||||
"opmlFile": "OPML File",
|
||||
"name": "Nome fonte",
|
||||
"editName": "Modifica Nome",
|
||||
"fetchFrequency": "Limite frecquenza di aggiornamento",
|
||||
"unlimited": "Senza limite",
|
||||
"openTarget": "Luogo predefinito di apertura degli articoli",
|
||||
"delete": "Elimina fonte",
|
||||
"add": "Aggiungi fonte",
|
||||
"import": "Importa",
|
||||
"export": "Esporta",
|
||||
"rssText": "RSS visualizza tutto il testo",
|
||||
"loadWebpage": "Carica pagina",
|
||||
"inputUrl": "Inserisci URL",
|
||||
"badIcon": "Icona non valida",
|
||||
"badUrl": "URL non valido",
|
||||
"deleteWarning": "La fonte è tutti i relativi articoli salvati verranno eliminati",
|
||||
"selected": "Seleziona fonte",
|
||||
"selectedMulti": "Seleziona più fonti"
|
||||
},
|
||||
"groups": {
|
||||
"exist": "Questo gruppo già esiste",
|
||||
"type": "Tipo",
|
||||
"group": "Gruppo",
|
||||
"source": "Fonte",
|
||||
"capacity": "Capacita",
|
||||
"exitGroup": "Ritorna ai gruppi",
|
||||
"deleteSource": "Rimuovi dal gruppo",
|
||||
"sourceHint": "Clicca e trascina le fonti per ordinarle",
|
||||
"create": "Crea gruppo",
|
||||
"selectedGroup": "Seleziona gruppo",
|
||||
"selectedSource": "Seleziona fonte",
|
||||
"enterName": "Inserisci nome",
|
||||
"editName": "Modificaname",
|
||||
"deleteGroup": "Elimina Gruppo",
|
||||
"chooseGroup": "Seleziona un gruppo",
|
||||
"addToGroup": "Aggiungi a ...",
|
||||
"groupHint": "Doppio-Click sul gruppo per modificare le fonti. Clicca e trascina le fonti per ordinarle"
|
||||
},
|
||||
"rules": {
|
||||
"intro": "automaticamentente seleziona articoli o manda notifiche tramite Regex",
|
||||
"help": "Per saperne di più",
|
||||
"source": "Fonte",
|
||||
"selectSource": "Seleziona una fonte",
|
||||
"new": "Nuova Regola",
|
||||
"if": "Se",
|
||||
"then": "Allora",
|
||||
"title": "Titolo",
|
||||
"content": "Contenuto",
|
||||
"fullSearch": "Titolo o Contenuto",
|
||||
"creator": "Autore",
|
||||
"match": "Risultati",
|
||||
"notMatch": "Non ci sono risultati",
|
||||
"regex": "Espressione Regolare",
|
||||
"badRegex": "Espressione Regolare Invalida",
|
||||
"action": "Azioni",
|
||||
"selectAction": "Seleziona azioni",
|
||||
"hint": "Le regole verranno applicate sequenzialmente, Clicca e trascina per riordinare",
|
||||
"test": "Prova le regole"
|
||||
},
|
||||
"service": {
|
||||
"intro": "Sincronizza attraverso i dispositivi i servizi RSS",
|
||||
"select": "Seleziona un servizio",
|
||||
"suggest": "Suggerisci un nuovo servizio",
|
||||
"overwriteWarning": "Le fonti locali verranno eliminate se sono gia presenti nel servizio ",
|
||||
"groupsWarning": "I gruppi non sono automaticamente sincronizzati con il servizio",
|
||||
"rateLimitWarning": "Per evitare un limite nella frecquenza di aggiornamento dei avere una API key personalizzata",
|
||||
"removeAd": "Rimuovi Annuncio",
|
||||
"endpoint": "Indirizzo",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"unchanged": "Non modificato",
|
||||
"fetchLimit": "Limite di Sincronizzazione",
|
||||
"fetchLimitNum": "{count} articoli recenti",
|
||||
"importGroups": "Importa gruppi",
|
||||
"failure": "Impossibile connettersi al servizio",
|
||||
"failureHint": "Controlla le impostazioni del servizio o se ci sono problemi di connessione",
|
||||
"fetchUnlimited": "Illimitato (non Raccomandaot)",
|
||||
"exportToLite": "Esporta a Fluent Reader Lite"
|
||||
},
|
||||
"app": {
|
||||
"cleanup": "Pulisci",
|
||||
"cache": "Elimina cache",
|
||||
"cacheSize": "Dimensione Cache {size}",
|
||||
"deleteChoices": "Rimuovi articoli di ... giorni fà",
|
||||
"confirmDelete": "Rimuovi",
|
||||
"daysAgo": "{days, plural, =1 {# giorno} other {# giorni}} fà",
|
||||
"deleteAll": "Rimuovi tutti gli articoli",
|
||||
"calculatingSize": "Calcolo dimensione...",
|
||||
"itemSize": "Gli articoli occupano {size} ",
|
||||
"confirmImport": "Vuoi veramente importare i dati dal file di backup? I dati correnti verranno eliminati",
|
||||
"data": "Memoria Applicazione",
|
||||
"backup": "Backup",
|
||||
"restore": "Ripristina",
|
||||
"frData": "Fluent Reader Data",
|
||||
"language": "Lingua Display",
|
||||
"theme": "Tema",
|
||||
"lightTheme": "Tema chiaro",
|
||||
"darkTheme": "Tema scuro",
|
||||
"enableProxy": "Abilita Proxy",
|
||||
"badUrl": "URL Invalido",
|
||||
"pac": "Indirizzo PAC",
|
||||
"setPac": " Imposta PAC",
|
||||
"pacHint": "Per proxies Socks, è raccomandato per i PAC di ritornare \"SOCKS5\" per proxy-side DNS. Disabilitare i proxy neccessita il riavvio dell'applicazione",
|
||||
"fetchInterval": "Ricarica intervallo Automaticamente",
|
||||
"never": "Mai"
|
||||
}
|
||||
}
|
||||
"allArticles": "Tutti gli articoli",
|
||||
"add": "Aggiungi",
|
||||
"create": "Crea",
|
||||
"icon": "Icona",
|
||||
"name": "Nome",
|
||||
"openExternal": "Apri Esternamente",
|
||||
"emptyName": "Questo campo non puo essere vuoto",
|
||||
"emptyField": "Questo campo non puo essere vuoto",
|
||||
"edit": "Modifica",
|
||||
"delete": "Elimina",
|
||||
"followSystem": "segui impostazioni di sistema",
|
||||
"more": "di più",
|
||||
"close": "Chiudi",
|
||||
"search": "Cerca",
|
||||
"loadMore": "Carica piu feed",
|
||||
"dangerButton": "Confermi di {action}?",
|
||||
"confirmMarkAll": "Vuoi veramente segnare tutti i feed di questa pagina come letti?",
|
||||
"confirm": "Confema",
|
||||
"cancel": "Anulla",
|
||||
"time": {
|
||||
"now": "ora",
|
||||
"m": "m",
|
||||
"h": "h",
|
||||
"d": "g",
|
||||
"minute": "{m, plural, =1 {# minuto} other {# minuti}}",
|
||||
"hour": "{h, plural, =1 {# ora} other {# ore}}",
|
||||
"day": "{d, plural, =1 {# giorno} other {# giorni}}"
|
||||
},
|
||||
"log": {
|
||||
"empty": "Non ci sono notifiche",
|
||||
"fetchFailure": "Errore nel caricare la fonte \"{name}\".",
|
||||
"fetchSuccess": "{count, plural, =1 {# articolo} other {# articoli}} caricato con successo",
|
||||
"networkError": "è occorso un errore di rete",
|
||||
"parseError": "è occorso un errore nel anallizzare il feed rss",
|
||||
"syncFailure": "Errore nel sicronizzarsi con il servizio"
|
||||
},
|
||||
"nav": {
|
||||
"menu": "Menu",
|
||||
"refresh": "Aggiorna",
|
||||
"markAllRead": "Segna tutti come letti",
|
||||
"notifications": "Notifiche",
|
||||
"view": "View",
|
||||
"settings": "Impostazioni",
|
||||
"minimize": "Riduci a Icona",
|
||||
"maximize": "Ingrandisci"
|
||||
},
|
||||
"menu": {
|
||||
"close": "Chiudi menu",
|
||||
"subscriptions": "Iscrizioni"
|
||||
},
|
||||
"article": {
|
||||
"error": "Errore nel caricare articolo",
|
||||
"reload": "Aggiorna?",
|
||||
"empty": "Non ci sono articoli",
|
||||
"untitled": "(Senza titolo)",
|
||||
"hide": "Nascondi articolo",
|
||||
"unhide": "Mostra articolo",
|
||||
"markRead": "Segna come letto",
|
||||
"markUnread": "Segna come non letto",
|
||||
"markAbove": "Segna precedenti come letti",
|
||||
"markBelow": "Segna successivi come letti",
|
||||
"star": "salva",
|
||||
"unstar": "rimuovi dai salvati",
|
||||
"fontSize": "Dimensione testo",
|
||||
"loadWebpage": "Carica pagina",
|
||||
"loadFull": "Carica tutto il contenuto",
|
||||
"notify": "Notifica se caricato in background",
|
||||
"dontNotify": "Non notificare"
|
||||
},
|
||||
"context": {
|
||||
"share": "Convidi",
|
||||
"read": "Leggi",
|
||||
"copyTitle": "Copia titolo",
|
||||
"copyURL": "Copia link",
|
||||
"copy": "Copia",
|
||||
"search": "Cerca \"{text}\" on {engine}",
|
||||
"view": "Visualizza",
|
||||
"cardView": "Card view",
|
||||
"listView": "List view",
|
||||
"magazineView": "Magazine view",
|
||||
"compactView": "Compact view",
|
||||
"filter": "Filtra",
|
||||
"unreadOnly": "Solo non letti",
|
||||
"starredOnly": "Solo Salvati",
|
||||
"fullSearch": "Cerca in tutto il lesto",
|
||||
"showHidden": "Visualizza articoli nascosti",
|
||||
"manageSources": "Gestisci fonti",
|
||||
"saveImageAs": "Salva immagine come …",
|
||||
"copyImage": "Copia immagine",
|
||||
"copyImageURL": "Copia immagine link",
|
||||
"caseSensitive": "Case sensitive",
|
||||
"showCover": "Mostra copertina",
|
||||
"showSnippet": "Show snippet",
|
||||
"fadeRead": "Scolorisci articoli letti"
|
||||
},
|
||||
"searchEngine": {
|
||||
"name": "Motore di ricerca",
|
||||
"google": "Google",
|
||||
"bing": "Bing",
|
||||
"baidu": "Baidu",
|
||||
"duckduckgo": "DuckDuckGo"
|
||||
},
|
||||
"settings": {
|
||||
"writeError": "è occorso un errore nello scrivere il file",
|
||||
"name": "Impostazioni",
|
||||
"fetching": "Aggiornamento delle fonti..Attendi",
|
||||
"exit": "Esci dalle Impostazioni",
|
||||
"sources": "Fonti",
|
||||
"grouping": "Gruppi",
|
||||
"rules": "Regole",
|
||||
"service": "Servizio",
|
||||
"app": "Preferenze",
|
||||
"about": "Informazioni",
|
||||
"version": "Versione",
|
||||
"shortcuts": "Shortcuts",
|
||||
"openSource": "Open source",
|
||||
"feedback": "Feedback"
|
||||
},
|
||||
"sources": {
|
||||
"serviceWarning": "le fonti importate o aggiunte qui non saranno sincronizzate con il tuo servizio",
|
||||
"serviceManaged": "la fonte è gesita dal tuo servizio",
|
||||
"untitled": "Fonte",
|
||||
"errorAdd": "Un errore è occorso nel caricare la fonte",
|
||||
"errorParse": "Un errore è occorso nell analizzare il file OPML",
|
||||
"errorParseInt": "Assicurati che il file non sia corrotto è sia in formato utf-8",
|
||||
"errorImport": "Errore nel importare {count, plural, =1 {# fonte} other {# fonti}}.",
|
||||
"exist": "Questa fonte è gia stata aggiunta",
|
||||
"opmlFile": "OPML File",
|
||||
"name": "Nome fonte",
|
||||
"editName": "Modifica Nome",
|
||||
"fetchFrequency": "Limite frecquenza di aggiornamento",
|
||||
"unlimited": "Senza limite",
|
||||
"openTarget": "Luogo predefinito di apertura degli articoli",
|
||||
"delete": "Elimina fonte",
|
||||
"add": "Aggiungi fonte",
|
||||
"import": "Importa",
|
||||
"export": "Esporta",
|
||||
"rssText": "RSS visualizza tutto il testo",
|
||||
"loadWebpage": "Carica pagina",
|
||||
"inputUrl": "Inserisci URL",
|
||||
"badIcon": "Icona non valida",
|
||||
"badUrl": "URL non valido",
|
||||
"deleteWarning": "La fonte è tutti i relativi articoli salvati verranno eliminati",
|
||||
"selected": "Seleziona fonte",
|
||||
"selectedMulti": "Seleziona più fonti"
|
||||
},
|
||||
"groups": {
|
||||
"exist": "Questo gruppo già esiste",
|
||||
"type": "Tipo",
|
||||
"group": "Gruppo",
|
||||
"source": "Fonte",
|
||||
"capacity": "Capacita",
|
||||
"exitGroup": "Ritorna ai gruppi",
|
||||
"deleteSource": "Rimuovi dal gruppo",
|
||||
"sourceHint": "Clicca e trascina le fonti per ordinarle",
|
||||
"create": "Crea gruppo",
|
||||
"selectedGroup": "Seleziona gruppo",
|
||||
"selectedSource": "Seleziona fonte",
|
||||
"enterName": "Inserisci nome",
|
||||
"editName": "Modificaname",
|
||||
"deleteGroup": "Elimina Gruppo",
|
||||
"chooseGroup": "Seleziona un gruppo",
|
||||
"addToGroup": "Aggiungi a ...",
|
||||
"groupHint": "Doppio-Click sul gruppo per modificare le fonti. Clicca e trascina le fonti per ordinarle"
|
||||
},
|
||||
"rules": {
|
||||
"intro": "automaticamentente seleziona articoli o manda notifiche tramite Regex",
|
||||
"help": "Per saperne di più",
|
||||
"source": "Fonte",
|
||||
"selectSource": "Seleziona una fonte",
|
||||
"new": "Nuova Regola",
|
||||
"if": "Se",
|
||||
"then": "Allora",
|
||||
"title": "Titolo",
|
||||
"content": "Contenuto",
|
||||
"fullSearch": "Titolo o Contenuto",
|
||||
"creator": "Autore",
|
||||
"match": "Risultati",
|
||||
"notMatch": "Non ci sono risultati",
|
||||
"regex": "Espressione Regolare",
|
||||
"badRegex": "Espressione Regolare Invalida",
|
||||
"action": "Azioni",
|
||||
"selectAction": "Seleziona azioni",
|
||||
"hint": "Le regole verranno applicate sequenzialmente, Clicca e trascina per riordinare",
|
||||
"test": "Prova le regole"
|
||||
},
|
||||
"service": {
|
||||
"intro": "Sincronizza attraverso i dispositivi i servizi RSS",
|
||||
"select": "Seleziona un servizio",
|
||||
"suggest": "Suggerisci un nuovo servizio",
|
||||
"overwriteWarning": "Le fonti locali verranno eliminate se sono gia presenti nel servizio ",
|
||||
"groupsWarning": "I gruppi non sono automaticamente sincronizzati con il servizio",
|
||||
"rateLimitWarning": "Per evitare un limite nella frecquenza di aggiornamento dei avere una API key personalizzata",
|
||||
"removeAd": "Rimuovi Annuncio",
|
||||
"endpoint": "Indirizzo",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"unchanged": "Non modificato",
|
||||
"fetchLimit": "Limite di Sincronizzazione",
|
||||
"fetchLimitNum": "{count} articoli recenti",
|
||||
"importGroups": "Importa gruppi",
|
||||
"failure": "Impossibile connettersi al servizio",
|
||||
"failureHint": "Controlla le impostazioni del servizio o se ci sono problemi di connessione",
|
||||
"fetchUnlimited": "Illimitato (non Raccomandaot)",
|
||||
"exportToLite": "Esporta a Fluent Reader Lite"
|
||||
},
|
||||
"app": {
|
||||
"cleanup": "Pulisci",
|
||||
"cache": "Elimina cache",
|
||||
"cacheSize": "Dimensione Cache {size}",
|
||||
"deleteChoices": "Rimuovi articoli di ... giorni fà",
|
||||
"confirmDelete": "Rimuovi",
|
||||
"daysAgo": "{days, plural, =1 {# giorno} other {# giorni}} fà",
|
||||
"deleteAll": "Rimuovi tutti gli articoli",
|
||||
"calculatingSize": "Calcolo dimensione...",
|
||||
"itemSize": "Gli articoli occupano {size} ",
|
||||
"confirmImport": "Vuoi veramente importare i dati dal file di backup? I dati correnti verranno eliminati",
|
||||
"data": "Memoria Applicazione",
|
||||
"backup": "Backup",
|
||||
"restore": "Ripristina",
|
||||
"frData": "Fluent Reader Data",
|
||||
"language": "Lingua Display",
|
||||
"theme": "Tema",
|
||||
"lightTheme": "Tema chiaro",
|
||||
"darkTheme": "Tema scuro",
|
||||
"enableProxy": "Abilita Proxy",
|
||||
"badUrl": "URL Invalido",
|
||||
"pac": "Indirizzo PAC",
|
||||
"setPac": " Imposta PAC",
|
||||
"pacHint": "Per proxies Socks, è raccomandato per i PAC di ritornare \"SOCKS5\" per proxy-side DNS. Disabilitare i proxy neccessita il riavvio dell'applicazione",
|
||||
"fetchInterval": "Ricarica intervallo Automaticamente",
|
||||
"never": "Mai"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -232,4 +232,4 @@
|
|||
"fetchInterval": "フェッチ間隔",
|
||||
"never": "しない"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -232,4 +232,4 @@
|
|||
"fetchInterval": "Automatisch ophalen",
|
||||
"never": "Nooit"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -232,4 +232,4 @@
|
|||
"fetchInterval": "Intervalo de atualização automática",
|
||||
"never": "Nunca"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -229,4 +229,4 @@
|
|||
"fetchInterval": "Otomatik getirme aralığı",
|
||||
"never": "Asla"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -230,4 +230,4 @@
|
|||
"fetchInterval": "自动抓取频率",
|
||||
"never": "从不"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,233 +1,233 @@
|
|||
{
|
||||
"allArticles": "全部文章",
|
||||
"add": "新增",
|
||||
"create": "新建",
|
||||
"icon": "圖示",
|
||||
"name": "名稱",
|
||||
"openExternal": "在瀏覽器中開啟",
|
||||
"emptyName": "名稱不得為空",
|
||||
"emptyField": "此項不得為空",
|
||||
"edit": "編輯",
|
||||
"delete": "刪除",
|
||||
"followSystem": "跟隨系統",
|
||||
"more": "更多",
|
||||
"close": "關閉",
|
||||
"search": "搜尋",
|
||||
"loadMore": "載入更多",
|
||||
"dangerButton": "確認{action}?",
|
||||
"confirmMarkAll": "確認將本頁所有文章標為已讀?",
|
||||
"confirm": "確認",
|
||||
"cancel": "取消",
|
||||
"time": {
|
||||
"now": "now",
|
||||
"m": "m",
|
||||
"h": "h",
|
||||
"d": "d",
|
||||
"minute": "{m}分鐘",
|
||||
"hour": "{h}小時",
|
||||
"day": "{d}天"
|
||||
},
|
||||
"log": {
|
||||
"empty": "無訊息",
|
||||
"fetchFailure": "無法載入訂閱源“{name}”",
|
||||
"fetchSuccess": "成功載入 {count} 篇文章",
|
||||
"networkError": "連線訂閱源時出錯",
|
||||
"parseError": "解析XML資訊流時出錯",
|
||||
"syncFailure": "無法與服務同步"
|
||||
},
|
||||
"nav": {
|
||||
"menu": "選單",
|
||||
"refresh": "重新整理",
|
||||
"markAllRead": "全部標為已讀",
|
||||
"notifications": "訊息",
|
||||
"view": "檢視",
|
||||
"settings": "選項",
|
||||
"minimize": "最小化",
|
||||
"maximize": "最大化"
|
||||
},
|
||||
"menu": {
|
||||
"close": "關閉選單",
|
||||
"subscriptions": "訂閱源"
|
||||
},
|
||||
"article": {
|
||||
"error": "文章載入失敗",
|
||||
"reload": "重新載入",
|
||||
"empty": "無文章",
|
||||
"untitled": "(無標題)",
|
||||
"hide": "隱藏文章",
|
||||
"unhide": "取消隱藏",
|
||||
"markRead": "標為已讀",
|
||||
"markUnread": "標為未讀",
|
||||
"markAbove": "將以上標為已讀",
|
||||
"markBelow": "將以下標為已讀",
|
||||
"star": "標為星標",
|
||||
"unstar": "取消星標",
|
||||
"fontSize": "字型大小",
|
||||
"loadWebpage": "載入網頁",
|
||||
"loadFull": "抓取全文",
|
||||
"notify": "後臺抓取時傳送通知",
|
||||
"dontNotify": "不傳送通知"
|
||||
},
|
||||
"context": {
|
||||
"share": "分享",
|
||||
"read": "閱讀",
|
||||
"copyTitle": "複製標題",
|
||||
"copyURL": "複製連結",
|
||||
"copy": "複製",
|
||||
"search": "使用 {engine} 搜尋“{text}”",
|
||||
"view": "檢視",
|
||||
"cardView": "卡片檢視",
|
||||
"listView": "列表檢視",
|
||||
"magazineView": "雜誌檢視",
|
||||
"compactView": "緊湊檢視",
|
||||
"filter": "篩選",
|
||||
"unreadOnly": "僅未讀文章",
|
||||
"starredOnly": "僅星標文章",
|
||||
"fullSearch": "在正文中搜尋",
|
||||
"showHidden": "顯示隱藏文章",
|
||||
"manageSources": "管理訂閱源",
|
||||
"saveImageAs": "將影象另存為",
|
||||
"copyImage": "複製影象",
|
||||
"copyImageURL": "複製影象連結",
|
||||
"caseSensitive": "區分大小寫",
|
||||
"showCover": "顯示封面",
|
||||
"showSnippet": "顯示摘要",
|
||||
"fadeRead": "淡化已讀文章"
|
||||
},
|
||||
"searchEngine": {
|
||||
"name": "搜尋引擎",
|
||||
"bing": "必應",
|
||||
"baidu": "百度"
|
||||
},
|
||||
"settings": {
|
||||
"writeError": "寫入檔案時發生錯誤",
|
||||
"name": "選項",
|
||||
"fetching": "正在更新訂閱源,請稍候…",
|
||||
"exit": "退出選項",
|
||||
"sources": "訂閱源",
|
||||
"grouping": "分組與排序",
|
||||
"rules": "規則",
|
||||
"service": "服務",
|
||||
"app": "應用偏好",
|
||||
"about": "關於",
|
||||
"version": "版本",
|
||||
"shortcuts": "快捷鍵",
|
||||
"openSource": "開源項目",
|
||||
"feedback": "反饋"
|
||||
},
|
||||
"sources": {
|
||||
"serviceWarning": "此處匯入或新增的訂閱源將不會與服務端同步",
|
||||
"serviceManaged": "該訂閱源由服務端管理",
|
||||
"untitled": "訂閱源",
|
||||
"errorAdd": "新增訂閱源時出錯",
|
||||
"errorParse": "解析OPML檔案時出錯",
|
||||
"errorParseHint": "請確保OPML檔案完整且使用UTF-8編碼。",
|
||||
"errorImport": "匯入{count}項訂閱源時出錯",
|
||||
"exist": "該訂閱源已存在",
|
||||
"opmlFile": "OPML檔案",
|
||||
"name": "訂閱源名稱",
|
||||
"editName": "修改名稱",
|
||||
"fetchFrequency": "抓取頻率限制",
|
||||
"unlimited": "無限制",
|
||||
"openTarget": "訂閱源文章開啟方式",
|
||||
"delete": "刪除訂閱源",
|
||||
"add": "新增訂閱源",
|
||||
"import": "匯入檔案",
|
||||
"export": "匯出檔案",
|
||||
"rssText": "RSS正文",
|
||||
"loadWebpage": "載入網頁",
|
||||
"inputUrl": "輸入URL",
|
||||
"badIcon": "圖示不存在或非圖片",
|
||||
"badUrl": "請正確輸入URL",
|
||||
"deleteWarning": "這將移除訂閱源與所有已儲存的文章",
|
||||
"selected": "選中訂閱源",
|
||||
"selectedMulti": "選中多個訂閱源"
|
||||
},
|
||||
"groups": {
|
||||
"exist": "該分組已存在",
|
||||
"type": "類型",
|
||||
"group": "分組",
|
||||
"source": "訂閱源",
|
||||
"capacity": "容量",
|
||||
"exitGroup": "退出分組",
|
||||
"deleteSource": "從分組刪除訂閱源",
|
||||
"sourceHint": "拖拽訂閱源以排序",
|
||||
"create": "新建分組",
|
||||
"selectedGroup": "選中分組",
|
||||
"selectedSource": "選中訂閱源",
|
||||
"enterName": "輸入名稱",
|
||||
"editName": "修改名稱",
|
||||
"deleteGroup": "刪除分組",
|
||||
"chooseGroup": "選擇分組",
|
||||
"addToGroup": "新增至分組",
|
||||
"groupHint": "雙擊分組以修改訂閱源,可通過拖拽排序"
|
||||
},
|
||||
"rules": {
|
||||
"intro": "通過正規表示式自動標記文章或推送通知",
|
||||
"help": "瞭解更多",
|
||||
"source": "訂閱源",
|
||||
"selectSource": "選擇一個訂閱源",
|
||||
"new": "新建規則",
|
||||
"if": "若",
|
||||
"then": "則",
|
||||
"title": "標題",
|
||||
"content": "正文",
|
||||
"fullSearch": "標題或正文",
|
||||
"creator": "作者",
|
||||
"match": "匹配",
|
||||
"notMatch": "不匹配",
|
||||
"regex": "正規表示式",
|
||||
"badRegex": "正規表示式非法",
|
||||
"action": "行為",
|
||||
"selectAction": "選擇行為",
|
||||
"hint": "規則將按順序執行,拖拽以排序",
|
||||
"test": "測試規則"
|
||||
},
|
||||
"service": {
|
||||
"intro": "通過 RSS 服務跨裝置保持同步",
|
||||
"select": "選擇服務",
|
||||
"suggest": "建議一項新服務",
|
||||
"overwriteWarning": "若本地與服務端存在URL相同的訂閱源,則本地訂閱源將被刪除",
|
||||
"groupsWarning": "分組不會自動與服務端保持同步",
|
||||
"rateLimitWarning": "為避免限流,您需要新建自己的 API Key",
|
||||
"removeAd": "移除廣告",
|
||||
"endpoint": "端點",
|
||||
"username": "使用者名稱",
|
||||
"password": "密碼",
|
||||
"unchanged": "未更改",
|
||||
"fetchLimit": "同步數量",
|
||||
"fetchLimitNum": "最近 {count} 篇文章",
|
||||
"importGroups": "匯入分組",
|
||||
"failure": "連線到服務時出錯",
|
||||
"failureHint": "請檢查服務配置或網路連線",
|
||||
"fetchUnlimited": "無限制(不建議)",
|
||||
"exportToLite": "匯出至 Fluent Reader Lite"
|
||||
},
|
||||
"app": {
|
||||
"cleanup": "清理",
|
||||
"cache": "清空快取",
|
||||
"cacheSize": "已快取{size}資料",
|
||||
"deleteChoices": "刪除 … 天前的文章",
|
||||
"confirmDelete": "刪除文章",
|
||||
"daysAgo": "{days} 天前",
|
||||
"deleteAll": "刪除全部文章",
|
||||
"calculatingSize": "正在計算佔用空間…",
|
||||
"itemSize": "本地文章約佔用{size}空間",
|
||||
"confirmImport": "確認要從備份檔案匯入資料嗎?這將清除所有應用資料。",
|
||||
"data": "應用資料",
|
||||
"backup": "備份",
|
||||
"restore": "還原",
|
||||
"frData": "Fluent Reader資料",
|
||||
"language": "介面語言",
|
||||
"theme": "應用主題",
|
||||
"lightTheme": "淺色模式",
|
||||
"darkTheme": "深色模式",
|
||||
"enableProxy": "啟用代理",
|
||||
"badUrl": "請正確輸入URL",
|
||||
"pac": "PAC地址",
|
||||
"setPac": "設定PAC",
|
||||
"pacHint": "對於Socks代理建議PAC返回“SOCKS5”以啟用代理端解析。關閉代理需重啟應用後生效。",
|
||||
"fetchInterval": "自動抓取頻率",
|
||||
"never": "從不"
|
||||
}
|
||||
}
|
||||
"allArticles": "全部文章",
|
||||
"add": "新增",
|
||||
"create": "新建",
|
||||
"icon": "圖示",
|
||||
"name": "名稱",
|
||||
"openExternal": "在瀏覽器中開啟",
|
||||
"emptyName": "名稱不得為空",
|
||||
"emptyField": "此項不得為空",
|
||||
"edit": "編輯",
|
||||
"delete": "刪除",
|
||||
"followSystem": "跟隨系統",
|
||||
"more": "更多",
|
||||
"close": "關閉",
|
||||
"search": "搜尋",
|
||||
"loadMore": "載入更多",
|
||||
"dangerButton": "確認{action}?",
|
||||
"confirmMarkAll": "確認將本頁所有文章標為已讀?",
|
||||
"confirm": "確認",
|
||||
"cancel": "取消",
|
||||
"time": {
|
||||
"now": "now",
|
||||
"m": "m",
|
||||
"h": "h",
|
||||
"d": "d",
|
||||
"minute": "{m}分鐘",
|
||||
"hour": "{h}小時",
|
||||
"day": "{d}天"
|
||||
},
|
||||
"log": {
|
||||
"empty": "無訊息",
|
||||
"fetchFailure": "無法載入訂閱源“{name}”",
|
||||
"fetchSuccess": "成功載入 {count} 篇文章",
|
||||
"networkError": "連線訂閱源時出錯",
|
||||
"parseError": "解析XML資訊流時出錯",
|
||||
"syncFailure": "無法與服務同步"
|
||||
},
|
||||
"nav": {
|
||||
"menu": "選單",
|
||||
"refresh": "重新整理",
|
||||
"markAllRead": "全部標為已讀",
|
||||
"notifications": "訊息",
|
||||
"view": "檢視",
|
||||
"settings": "選項",
|
||||
"minimize": "最小化",
|
||||
"maximize": "最大化"
|
||||
},
|
||||
"menu": {
|
||||
"close": "關閉選單",
|
||||
"subscriptions": "訂閱源"
|
||||
},
|
||||
"article": {
|
||||
"error": "文章載入失敗",
|
||||
"reload": "重新載入",
|
||||
"empty": "無文章",
|
||||
"untitled": "(無標題)",
|
||||
"hide": "隱藏文章",
|
||||
"unhide": "取消隱藏",
|
||||
"markRead": "標為已讀",
|
||||
"markUnread": "標為未讀",
|
||||
"markAbove": "將以上標為已讀",
|
||||
"markBelow": "將以下標為已讀",
|
||||
"star": "標為星標",
|
||||
"unstar": "取消星標",
|
||||
"fontSize": "字型大小",
|
||||
"loadWebpage": "載入網頁",
|
||||
"loadFull": "抓取全文",
|
||||
"notify": "後臺抓取時傳送通知",
|
||||
"dontNotify": "不傳送通知"
|
||||
},
|
||||
"context": {
|
||||
"share": "分享",
|
||||
"read": "閱讀",
|
||||
"copyTitle": "複製標題",
|
||||
"copyURL": "複製連結",
|
||||
"copy": "複製",
|
||||
"search": "使用 {engine} 搜尋“{text}”",
|
||||
"view": "檢視",
|
||||
"cardView": "卡片檢視",
|
||||
"listView": "列表檢視",
|
||||
"magazineView": "雜誌檢視",
|
||||
"compactView": "緊湊檢視",
|
||||
"filter": "篩選",
|
||||
"unreadOnly": "僅未讀文章",
|
||||
"starredOnly": "僅星標文章",
|
||||
"fullSearch": "在正文中搜尋",
|
||||
"showHidden": "顯示隱藏文章",
|
||||
"manageSources": "管理訂閱源",
|
||||
"saveImageAs": "將影象另存為",
|
||||
"copyImage": "複製影象",
|
||||
"copyImageURL": "複製影象連結",
|
||||
"caseSensitive": "區分大小寫",
|
||||
"showCover": "顯示封面",
|
||||
"showSnippet": "顯示摘要",
|
||||
"fadeRead": "淡化已讀文章"
|
||||
},
|
||||
"searchEngine": {
|
||||
"name": "搜尋引擎",
|
||||
"bing": "必應",
|
||||
"baidu": "百度"
|
||||
},
|
||||
"settings": {
|
||||
"writeError": "寫入檔案時發生錯誤",
|
||||
"name": "選項",
|
||||
"fetching": "正在更新訂閱源,請稍候…",
|
||||
"exit": "退出選項",
|
||||
"sources": "訂閱源",
|
||||
"grouping": "分組與排序",
|
||||
"rules": "規則",
|
||||
"service": "服務",
|
||||
"app": "應用偏好",
|
||||
"about": "關於",
|
||||
"version": "版本",
|
||||
"shortcuts": "快捷鍵",
|
||||
"openSource": "開源項目",
|
||||
"feedback": "反饋"
|
||||
},
|
||||
"sources": {
|
||||
"serviceWarning": "此處匯入或新增的訂閱源將不會與服務端同步",
|
||||
"serviceManaged": "該訂閱源由服務端管理",
|
||||
"untitled": "訂閱源",
|
||||
"errorAdd": "新增訂閱源時出錯",
|
||||
"errorParse": "解析OPML檔案時出錯",
|
||||
"errorParseHint": "請確保OPML檔案完整且使用UTF-8編碼。",
|
||||
"errorImport": "匯入{count}項訂閱源時出錯",
|
||||
"exist": "該訂閱源已存在",
|
||||
"opmlFile": "OPML檔案",
|
||||
"name": "訂閱源名稱",
|
||||
"editName": "修改名稱",
|
||||
"fetchFrequency": "抓取頻率限制",
|
||||
"unlimited": "無限制",
|
||||
"openTarget": "訂閱源文章開啟方式",
|
||||
"delete": "刪除訂閱源",
|
||||
"add": "新增訂閱源",
|
||||
"import": "匯入檔案",
|
||||
"export": "匯出檔案",
|
||||
"rssText": "RSS正文",
|
||||
"loadWebpage": "載入網頁",
|
||||
"inputUrl": "輸入URL",
|
||||
"badIcon": "圖示不存在或非圖片",
|
||||
"badUrl": "請正確輸入URL",
|
||||
"deleteWarning": "這將移除訂閱源與所有已儲存的文章",
|
||||
"selected": "選中訂閱源",
|
||||
"selectedMulti": "選中多個訂閱源"
|
||||
},
|
||||
"groups": {
|
||||
"exist": "該分組已存在",
|
||||
"type": "類型",
|
||||
"group": "分組",
|
||||
"source": "訂閱源",
|
||||
"capacity": "容量",
|
||||
"exitGroup": "退出分組",
|
||||
"deleteSource": "從分組刪除訂閱源",
|
||||
"sourceHint": "拖拽訂閱源以排序",
|
||||
"create": "新建分組",
|
||||
"selectedGroup": "選中分組",
|
||||
"selectedSource": "選中訂閱源",
|
||||
"enterName": "輸入名稱",
|
||||
"editName": "修改名稱",
|
||||
"deleteGroup": "刪除分組",
|
||||
"chooseGroup": "選擇分組",
|
||||
"addToGroup": "新增至分組",
|
||||
"groupHint": "雙擊分組以修改訂閱源,可通過拖拽排序"
|
||||
},
|
||||
"rules": {
|
||||
"intro": "通過正規表示式自動標記文章或推送通知",
|
||||
"help": "瞭解更多",
|
||||
"source": "訂閱源",
|
||||
"selectSource": "選擇一個訂閱源",
|
||||
"new": "新建規則",
|
||||
"if": "若",
|
||||
"then": "則",
|
||||
"title": "標題",
|
||||
"content": "正文",
|
||||
"fullSearch": "標題或正文",
|
||||
"creator": "作者",
|
||||
"match": "匹配",
|
||||
"notMatch": "不匹配",
|
||||
"regex": "正規表示式",
|
||||
"badRegex": "正規表示式非法",
|
||||
"action": "行為",
|
||||
"selectAction": "選擇行為",
|
||||
"hint": "規則將按順序執行,拖拽以排序",
|
||||
"test": "測試規則"
|
||||
},
|
||||
"service": {
|
||||
"intro": "通過 RSS 服務跨裝置保持同步",
|
||||
"select": "選擇服務",
|
||||
"suggest": "建議一項新服務",
|
||||
"overwriteWarning": "若本地與服務端存在URL相同的訂閱源,則本地訂閱源將被刪除",
|
||||
"groupsWarning": "分組不會自動與服務端保持同步",
|
||||
"rateLimitWarning": "為避免限流,您需要新建自己的 API Key",
|
||||
"removeAd": "移除廣告",
|
||||
"endpoint": "端點",
|
||||
"username": "使用者名稱",
|
||||
"password": "密碼",
|
||||
"unchanged": "未更改",
|
||||
"fetchLimit": "同步數量",
|
||||
"fetchLimitNum": "最近 {count} 篇文章",
|
||||
"importGroups": "匯入分組",
|
||||
"failure": "連線到服務時出錯",
|
||||
"failureHint": "請檢查服務配置或網路連線",
|
||||
"fetchUnlimited": "無限制(不建議)",
|
||||
"exportToLite": "匯出至 Fluent Reader Lite"
|
||||
},
|
||||
"app": {
|
||||
"cleanup": "清理",
|
||||
"cache": "清空快取",
|
||||
"cacheSize": "已快取{size}資料",
|
||||
"deleteChoices": "刪除 … 天前的文章",
|
||||
"confirmDelete": "刪除文章",
|
||||
"daysAgo": "{days} 天前",
|
||||
"deleteAll": "刪除全部文章",
|
||||
"calculatingSize": "正在計算佔用空間…",
|
||||
"itemSize": "本地文章約佔用{size}空間",
|
||||
"confirmImport": "確認要從備份檔案匯入資料嗎?這將清除所有應用資料。",
|
||||
"data": "應用資料",
|
||||
"backup": "備份",
|
||||
"restore": "還原",
|
||||
"frData": "Fluent Reader資料",
|
||||
"language": "介面語言",
|
||||
"theme": "應用主題",
|
||||
"lightTheme": "淺色模式",
|
||||
"darkTheme": "深色模式",
|
||||
"enableProxy": "啟用代理",
|
||||
"badUrl": "請正確輸入URL",
|
||||
"pac": "PAC地址",
|
||||
"setPac": "設定PAC",
|
||||
"pacHint": "對於Socks代理建議PAC返回“SOCKS5”以啟用代理端解析。關閉代理需重啟應用後生效。",
|
||||
"fetchInterval": "自動抓取頻率",
|
||||
"never": "從不"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,56 @@
|
|||
import intl from "react-intl-universal"
|
||||
import { INIT_SOURCES, SourceActionTypes, ADD_SOURCE, UPDATE_SOURCE, DELETE_SOURCE, initSources, SourceOpenTarget, updateFavicon } from "./source"
|
||||
import {
|
||||
INIT_SOURCES,
|
||||
SourceActionTypes,
|
||||
ADD_SOURCE,
|
||||
UPDATE_SOURCE,
|
||||
DELETE_SOURCE,
|
||||
initSources,
|
||||
SourceOpenTarget,
|
||||
updateFavicon,
|
||||
} from "./source"
|
||||
import { RSSItem, ItemActionTypes, FETCH_ITEMS, fetchItems } from "./item"
|
||||
import { ActionStatus, AppThunk, getWindowBreakpoint, initTouchBarWithTexts } from "../utils"
|
||||
import {
|
||||
ActionStatus,
|
||||
AppThunk,
|
||||
getWindowBreakpoint,
|
||||
initTouchBarWithTexts,
|
||||
} from "../utils"
|
||||
import { INIT_FEEDS, FeedActionTypes, ALL, initFeeds } from "./feed"
|
||||
import { SourceGroupActionTypes, UPDATE_SOURCE_GROUP, ADD_SOURCE_TO_GROUP, DELETE_SOURCE_GROUP, REMOVE_SOURCE_FROM_GROUP, REORDER_SOURCE_GROUPS } from "./group"
|
||||
import { PageActionTypes, SELECT_PAGE, PageType, selectAllArticles, showItemFromId } from "./page"
|
||||
import {
|
||||
SourceGroupActionTypes,
|
||||
UPDATE_SOURCE_GROUP,
|
||||
ADD_SOURCE_TO_GROUP,
|
||||
DELETE_SOURCE_GROUP,
|
||||
REMOVE_SOURCE_FROM_GROUP,
|
||||
REORDER_SOURCE_GROUPS,
|
||||
} from "./group"
|
||||
import {
|
||||
PageActionTypes,
|
||||
SELECT_PAGE,
|
||||
PageType,
|
||||
selectAllArticles,
|
||||
showItemFromId,
|
||||
} from "./page"
|
||||
import { getCurrentLocale } from "../settings"
|
||||
import locales from "../i18n/_locales"
|
||||
import { SYNC_SERVICE, ServiceActionTypes } from "./service"
|
||||
|
||||
export const enum ContextMenuType {
|
||||
Hidden, Item, Text, View, Group, Image, MarkRead
|
||||
Hidden,
|
||||
Item,
|
||||
Text,
|
||||
View,
|
||||
Group,
|
||||
Image,
|
||||
MarkRead,
|
||||
}
|
||||
|
||||
export const enum AppLogType {
|
||||
Info, Warning, Failure, Article
|
||||
Info,
|
||||
Warning,
|
||||
Failure,
|
||||
Article,
|
||||
}
|
||||
|
||||
export class AppLog {
|
||||
|
@ -24,7 +60,12 @@ export class AppLog {
|
|||
iid?: number
|
||||
time: Date
|
||||
|
||||
constructor(type: AppLogType, title: string, details: string=null, iid: number = null) {
|
||||
constructor(
|
||||
type: AppLogType,
|
||||
title: string,
|
||||
details: string = null,
|
||||
iid: number = null
|
||||
) {
|
||||
this.type = type
|
||||
this.title = title
|
||||
this.details = details
|
||||
|
@ -49,24 +90,24 @@ export class AppState {
|
|||
display: false,
|
||||
changed: false,
|
||||
sids: new Array<number>(),
|
||||
saving: false
|
||||
saving: false,
|
||||
}
|
||||
logMenu = {
|
||||
display: false,
|
||||
notify: false,
|
||||
logs: new Array<AppLog>()
|
||||
logs: new Array<AppLog>(),
|
||||
}
|
||||
|
||||
|
||||
contextMenu: {
|
||||
type: ContextMenuType,
|
||||
event?: MouseEvent | string,
|
||||
position?: [number, number],
|
||||
type: ContextMenuType
|
||||
event?: MouseEvent | string
|
||||
position?: [number, number]
|
||||
target?: [RSSItem, string] | number[] | [string, string]
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.contextMenu = {
|
||||
type: ContextMenuType.Hidden
|
||||
type: ContextMenuType.Hidden,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -115,14 +156,21 @@ interface OpenImageMenuAction {
|
|||
position: [number, number]
|
||||
}
|
||||
|
||||
export type ContextMenuActionTypes = CloseContextMenuAction | OpenItemMenuAction
|
||||
| OpenTextMenuAction | OpenViewMenuAction | OpenGroupMenuAction | OpenImageMenuAction
|
||||
export type ContextMenuActionTypes =
|
||||
| CloseContextMenuAction
|
||||
| OpenItemMenuAction
|
||||
| OpenTextMenuAction
|
||||
| OpenViewMenuAction
|
||||
| OpenGroupMenuAction
|
||||
| OpenImageMenuAction
|
||||
| OpenMarkAllMenuAction
|
||||
|
||||
export const TOGGLE_LOGS = "TOGGLE_LOGS"
|
||||
export const PUSH_NOTIFICATION = "PUSH_NOTIFICATION"
|
||||
|
||||
interface ToggleLogMenuAction { type: typeof TOGGLE_LOGS }
|
||||
interface ToggleLogMenuAction {
|
||||
type: typeof TOGGLE_LOGS
|
||||
}
|
||||
|
||||
interface PushNotificationAction {
|
||||
type: typeof PUSH_NOTIFICATION
|
||||
|
@ -155,7 +203,10 @@ interface FreeMemoryAction {
|
|||
type: typeof FREE_MEMORY
|
||||
iids: Set<number>
|
||||
}
|
||||
export type SettingsActionTypes = ToggleSettingsAction | SaveSettingsAction | FreeMemoryAction
|
||||
export type SettingsActionTypes =
|
||||
| ToggleSettingsAction
|
||||
| SaveSettingsAction
|
||||
| FreeMemoryAction
|
||||
|
||||
export function closeContextMenu(): AppThunk {
|
||||
return (dispatch, getState) => {
|
||||
|
@ -165,41 +216,58 @@ export function closeContextMenu(): AppThunk {
|
|||
}
|
||||
}
|
||||
|
||||
export function openItemMenu(item: RSSItem, feedId: string, event: React.MouseEvent): ContextMenuActionTypes {
|
||||
export function openItemMenu(
|
||||
item: RSSItem,
|
||||
feedId: string,
|
||||
event: React.MouseEvent
|
||||
): ContextMenuActionTypes {
|
||||
return {
|
||||
type: OPEN_ITEM_MENU,
|
||||
event: event.nativeEvent,
|
||||
item: item,
|
||||
feedId: feedId
|
||||
feedId: feedId,
|
||||
}
|
||||
}
|
||||
|
||||
export function openTextMenu(position: [number, number], text: string, url: string = null): ContextMenuActionTypes {
|
||||
export function openTextMenu(
|
||||
position: [number, number],
|
||||
text: string,
|
||||
url: string = null
|
||||
): ContextMenuActionTypes {
|
||||
return {
|
||||
type: OPEN_TEXT_MENU,
|
||||
position: position,
|
||||
item: [text, url]
|
||||
item: [text, url],
|
||||
}
|
||||
}
|
||||
|
||||
export const openViewMenu = (): ContextMenuActionTypes => ({ type: OPEN_VIEW_MENU })
|
||||
export const openViewMenu = (): ContextMenuActionTypes => ({
|
||||
type: OPEN_VIEW_MENU,
|
||||
})
|
||||
|
||||
export function openGroupMenu(sids: number[], event: React.MouseEvent): ContextMenuActionTypes {
|
||||
export function openGroupMenu(
|
||||
sids: number[],
|
||||
event: React.MouseEvent
|
||||
): ContextMenuActionTypes {
|
||||
return {
|
||||
type: OPEN_GROUP_MENU,
|
||||
event: event.nativeEvent,
|
||||
sids: sids
|
||||
sids: sids,
|
||||
}
|
||||
}
|
||||
|
||||
export function openImageMenu(position: [number, number]): ContextMenuActionTypes {
|
||||
export function openImageMenu(
|
||||
position: [number, number]
|
||||
): ContextMenuActionTypes {
|
||||
return {
|
||||
type: OPEN_IMAGE_MENU,
|
||||
position: position
|
||||
position: position,
|
||||
}
|
||||
}
|
||||
|
||||
export const openMarkAllMenu = (): ContextMenuActionTypes => ({ type: OPEN_MARK_ALL_MENU })
|
||||
export const openMarkAllMenu = (): ContextMenuActionTypes => ({
|
||||
type: OPEN_MARK_ALL_MENU,
|
||||
})
|
||||
|
||||
export function toggleMenu(): AppThunk {
|
||||
return (dispatch, getState) => {
|
||||
|
@ -241,7 +309,7 @@ function freeMemory(): AppThunk {
|
|||
}
|
||||
dispatch({
|
||||
type: FREE_MEMORY,
|
||||
iids: iids
|
||||
iids: iids,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -276,7 +344,10 @@ export function pushNotification(item: RSSItem): AppThunk {
|
|||
const notification = new Notification(item.title, options)
|
||||
notification.onclick = () => {
|
||||
const state = getState()
|
||||
if (state.sources[item.source].openTarget === SourceOpenTarget.External) {
|
||||
if (
|
||||
state.sources[item.source].openTarget ===
|
||||
SourceOpenTarget.External
|
||||
) {
|
||||
window.utils.openExternal(item.link)
|
||||
} else if (!state.app.settings.display) {
|
||||
window.utils.focus()
|
||||
|
@ -288,7 +359,7 @@ export function pushNotification(item: RSSItem): AppThunk {
|
|||
type: PUSH_NOTIFICATION,
|
||||
iid: item._id,
|
||||
title: item.title,
|
||||
source: sourceName
|
||||
source: sourceName,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -300,74 +371,95 @@ export interface InitIntlAction {
|
|||
}
|
||||
export const initIntlDone = (locale: string): InitIntlAction => ({
|
||||
type: INIT_INTL,
|
||||
locale: locale
|
||||
locale: locale,
|
||||
})
|
||||
|
||||
export function initIntl(): AppThunk<Promise<void>> {
|
||||
return (dispatch) => {
|
||||
return dispatch => {
|
||||
let locale = getCurrentLocale()
|
||||
return intl.init({
|
||||
currentLocale: locale,
|
||||
locales: locales,
|
||||
fallbackLocale: "en-US"
|
||||
}).then(() => { dispatch(initIntlDone(locale)) })
|
||||
return intl
|
||||
.init({
|
||||
currentLocale: locale,
|
||||
locales: locales,
|
||||
fallbackLocale: "en-US",
|
||||
})
|
||||
.then(() => {
|
||||
dispatch(initIntlDone(locale))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function initApp(): AppThunk {
|
||||
return (dispatch) => {
|
||||
return dispatch => {
|
||||
document.body.classList.add(window.utils.platform)
|
||||
dispatch(initIntl()).then(async () => {
|
||||
if (window.utils.platform === "darwin") initTouchBarWithTexts()
|
||||
await dispatch(initSources())
|
||||
}).then(() => dispatch(initFeeds()))
|
||||
.then(async () => {
|
||||
dispatch(selectAllArticles())
|
||||
await dispatch(fetchItems())
|
||||
}).then(() => {
|
||||
dispatch(updateFavicon())
|
||||
})
|
||||
dispatch(initIntl())
|
||||
.then(async () => {
|
||||
if (window.utils.platform === "darwin") initTouchBarWithTexts()
|
||||
await dispatch(initSources())
|
||||
})
|
||||
.then(() => dispatch(initFeeds()))
|
||||
.then(async () => {
|
||||
dispatch(selectAllArticles())
|
||||
await dispatch(fetchItems())
|
||||
})
|
||||
.then(() => {
|
||||
dispatch(updateFavicon())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function appReducer(
|
||||
state = new AppState(),
|
||||
action: SourceActionTypes | ItemActionTypes | ContextMenuActionTypes | SettingsActionTypes | InitIntlAction
|
||||
| MenuActionTypes | LogMenuActionType | FeedActionTypes | PageActionTypes | SourceGroupActionTypes
|
||||
action:
|
||||
| SourceActionTypes
|
||||
| ItemActionTypes
|
||||
| ContextMenuActionTypes
|
||||
| SettingsActionTypes
|
||||
| InitIntlAction
|
||||
| MenuActionTypes
|
||||
| LogMenuActionType
|
||||
| FeedActionTypes
|
||||
| PageActionTypes
|
||||
| SourceGroupActionTypes
|
||||
| ServiceActionTypes
|
||||
): AppState {
|
||||
switch (action.type) {
|
||||
case INIT_INTL: return {
|
||||
...state,
|
||||
locale: action.locale
|
||||
}
|
||||
case INIT_INTL:
|
||||
return {
|
||||
...state,
|
||||
locale: action.locale,
|
||||
}
|
||||
case INIT_SOURCES:
|
||||
switch (action.status) {
|
||||
case ActionStatus.Success: return {
|
||||
...state,
|
||||
sourceInit: true
|
||||
}
|
||||
default: return state
|
||||
case ActionStatus.Success:
|
||||
return {
|
||||
...state,
|
||||
sourceInit: true,
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
case ADD_SOURCE:
|
||||
case ADD_SOURCE:
|
||||
switch (action.status) {
|
||||
case ActionStatus.Request: return {
|
||||
...state,
|
||||
fetchingItems: true,
|
||||
settings: {
|
||||
...state.settings,
|
||||
changed: true,
|
||||
saving: true
|
||||
case ActionStatus.Request:
|
||||
return {
|
||||
...state,
|
||||
fetchingItems: true,
|
||||
settings: {
|
||||
...state.settings,
|
||||
changed: true,
|
||||
saving: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
default: return {
|
||||
...state,
|
||||
fetchingItems: state.fetchingTotal !== 0,
|
||||
settings: {
|
||||
...state.settings,
|
||||
saving: action.batch
|
||||
default:
|
||||
return {
|
||||
...state,
|
||||
fetchingItems: state.fetchingTotal !== 0,
|
||||
settings: {
|
||||
...state.settings,
|
||||
saving: action.batch,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
case UPDATE_SOURCE:
|
||||
case DELETE_SOURCE:
|
||||
|
@ -375,192 +467,243 @@ export function appReducer(
|
|||
case ADD_SOURCE_TO_GROUP:
|
||||
case REMOVE_SOURCE_FROM_GROUP:
|
||||
case REORDER_SOURCE_GROUPS:
|
||||
case DELETE_SOURCE_GROUP: return {
|
||||
...state,
|
||||
settings: {
|
||||
...state.settings,
|
||||
changed: true
|
||||
case DELETE_SOURCE_GROUP:
|
||||
return {
|
||||
...state,
|
||||
settings: {
|
||||
...state.settings,
|
||||
changed: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
case INIT_FEEDS:
|
||||
switch (action.status) {
|
||||
case ActionStatus.Request: return state
|
||||
default: return {
|
||||
...state,
|
||||
feedInit: true
|
||||
}
|
||||
case ActionStatus.Request:
|
||||
return state
|
||||
default:
|
||||
return {
|
||||
...state,
|
||||
feedInit: true,
|
||||
}
|
||||
}
|
||||
case SYNC_SERVICE:
|
||||
switch (action.status) {
|
||||
case ActionStatus.Request: return {
|
||||
...state,
|
||||
syncing: true
|
||||
}
|
||||
case ActionStatus.Failure: return {
|
||||
...state,
|
||||
syncing: false,
|
||||
logMenu: {
|
||||
...state.logMenu,
|
||||
notify: true,
|
||||
logs: [...state.logMenu.logs, new AppLog(
|
||||
AppLogType.Failure,
|
||||
intl.get("log.syncFailure"),
|
||||
String(action.err)
|
||||
)]
|
||||
case ActionStatus.Request:
|
||||
return {
|
||||
...state,
|
||||
syncing: true,
|
||||
}
|
||||
case ActionStatus.Failure:
|
||||
return {
|
||||
...state,
|
||||
syncing: false,
|
||||
logMenu: {
|
||||
...state.logMenu,
|
||||
notify: true,
|
||||
logs: [
|
||||
...state.logMenu.logs,
|
||||
new AppLog(
|
||||
AppLogType.Failure,
|
||||
intl.get("log.syncFailure"),
|
||||
String(action.err)
|
||||
),
|
||||
],
|
||||
},
|
||||
}
|
||||
default:
|
||||
return {
|
||||
...state,
|
||||
syncing: false,
|
||||
}
|
||||
}
|
||||
default: return {
|
||||
...state,
|
||||
syncing: false
|
||||
}
|
||||
}
|
||||
case FETCH_ITEMS:
|
||||
switch (action.status) {
|
||||
case ActionStatus.Request: return {
|
||||
...state,
|
||||
fetchingItems: true,
|
||||
fetchingProgress: 0,
|
||||
fetchingTotal: action.fetchCount
|
||||
}
|
||||
case ActionStatus.Failure: return {
|
||||
...state,
|
||||
logMenu: {
|
||||
...state.logMenu,
|
||||
notify: !state.logMenu.display,
|
||||
logs: [...state.logMenu.logs, new AppLog(
|
||||
AppLogType.Failure,
|
||||
intl.get("log.fetchFailure", { name: action.errSource.name }),
|
||||
String(action.err)
|
||||
)]
|
||||
case ActionStatus.Request:
|
||||
return {
|
||||
...state,
|
||||
fetchingItems: true,
|
||||
fetchingProgress: 0,
|
||||
fetchingTotal: action.fetchCount,
|
||||
}
|
||||
}
|
||||
case ActionStatus.Success: return {
|
||||
...state,
|
||||
fetchingItems: false,
|
||||
fetchingTotal: 0,
|
||||
logMenu: action.items.length == 0 ? state.logMenu : {
|
||||
...state.logMenu,
|
||||
logs: [...state.logMenu.logs, new AppLog(
|
||||
AppLogType.Info,
|
||||
intl.get("log.fetchSuccess", { count: action.items.length })
|
||||
)]
|
||||
case ActionStatus.Failure:
|
||||
return {
|
||||
...state,
|
||||
logMenu: {
|
||||
...state.logMenu,
|
||||
notify: !state.logMenu.display,
|
||||
logs: [
|
||||
...state.logMenu.logs,
|
||||
new AppLog(
|
||||
AppLogType.Failure,
|
||||
intl.get("log.fetchFailure", {
|
||||
name: action.errSource.name,
|
||||
}),
|
||||
String(action.err)
|
||||
),
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
case ActionStatus.Intermediate: return {
|
||||
...state,
|
||||
fetchingProgress: state.fetchingProgress + 1
|
||||
}
|
||||
default: return state
|
||||
case ActionStatus.Success:
|
||||
return {
|
||||
...state,
|
||||
fetchingItems: false,
|
||||
fetchingTotal: 0,
|
||||
logMenu:
|
||||
action.items.length == 0
|
||||
? state.logMenu
|
||||
: {
|
||||
...state.logMenu,
|
||||
logs: [
|
||||
...state.logMenu.logs,
|
||||
new AppLog(
|
||||
AppLogType.Info,
|
||||
intl.get("log.fetchSuccess", {
|
||||
count: action.items.length,
|
||||
})
|
||||
),
|
||||
],
|
||||
},
|
||||
}
|
||||
case ActionStatus.Intermediate:
|
||||
return {
|
||||
...state,
|
||||
fetchingProgress: state.fetchingProgress + 1,
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
case SELECT_PAGE:
|
||||
case SELECT_PAGE:
|
||||
switch (action.pageType) {
|
||||
case PageType.AllArticles: return {
|
||||
...state,
|
||||
menu: state.menu && action.keepMenu,
|
||||
menuKey: ALL,
|
||||
title: intl.get("allArticles")
|
||||
}
|
||||
case PageType.Sources: return {
|
||||
...state,
|
||||
menu: state.menu && action.keepMenu,
|
||||
menuKey: action.menuKey,
|
||||
title: action.title
|
||||
}
|
||||
case PageType.AllArticles:
|
||||
return {
|
||||
...state,
|
||||
menu: state.menu && action.keepMenu,
|
||||
menuKey: ALL,
|
||||
title: intl.get("allArticles"),
|
||||
}
|
||||
case PageType.Sources:
|
||||
return {
|
||||
...state,
|
||||
menu: state.menu && action.keepMenu,
|
||||
menuKey: action.menuKey,
|
||||
title: action.title,
|
||||
}
|
||||
}
|
||||
case CLOSE_CONTEXT_MENU: return {
|
||||
...state,
|
||||
contextMenu: {
|
||||
type: ContextMenuType.Hidden
|
||||
case CLOSE_CONTEXT_MENU:
|
||||
return {
|
||||
...state,
|
||||
contextMenu: {
|
||||
type: ContextMenuType.Hidden,
|
||||
},
|
||||
}
|
||||
}
|
||||
case OPEN_ITEM_MENU: return {
|
||||
...state,
|
||||
contextMenu: {
|
||||
type: ContextMenuType.Item,
|
||||
event: action.event,
|
||||
target: [action.item, action.feedId]
|
||||
case OPEN_ITEM_MENU:
|
||||
return {
|
||||
...state,
|
||||
contextMenu: {
|
||||
type: ContextMenuType.Item,
|
||||
event: action.event,
|
||||
target: [action.item, action.feedId],
|
||||
},
|
||||
}
|
||||
}
|
||||
case OPEN_TEXT_MENU: return {
|
||||
...state,
|
||||
contextMenu: {
|
||||
type: ContextMenuType.Text,
|
||||
position: action.position,
|
||||
target: action.item
|
||||
case OPEN_TEXT_MENU:
|
||||
return {
|
||||
...state,
|
||||
contextMenu: {
|
||||
type: ContextMenuType.Text,
|
||||
position: action.position,
|
||||
target: action.item,
|
||||
},
|
||||
}
|
||||
}
|
||||
case OPEN_VIEW_MENU: return {
|
||||
...state,
|
||||
contextMenu: {
|
||||
type: state.contextMenu.type === ContextMenuType.View
|
||||
? ContextMenuType.Hidden : ContextMenuType.View,
|
||||
event: "#view-toggle"
|
||||
case OPEN_VIEW_MENU:
|
||||
return {
|
||||
...state,
|
||||
contextMenu: {
|
||||
type:
|
||||
state.contextMenu.type === ContextMenuType.View
|
||||
? ContextMenuType.Hidden
|
||||
: ContextMenuType.View,
|
||||
event: "#view-toggle",
|
||||
},
|
||||
}
|
||||
}
|
||||
case OPEN_GROUP_MENU: return {
|
||||
...state,
|
||||
contextMenu: {
|
||||
type: ContextMenuType.Group,
|
||||
event: action.event,
|
||||
target: action.sids
|
||||
case OPEN_GROUP_MENU:
|
||||
return {
|
||||
...state,
|
||||
contextMenu: {
|
||||
type: ContextMenuType.Group,
|
||||
event: action.event,
|
||||
target: action.sids,
|
||||
},
|
||||
}
|
||||
}
|
||||
case OPEN_IMAGE_MENU: return {
|
||||
...state,
|
||||
contextMenu: {
|
||||
type: ContextMenuType.Image,
|
||||
position: action.position
|
||||
case OPEN_IMAGE_MENU:
|
||||
return {
|
||||
...state,
|
||||
contextMenu: {
|
||||
type: ContextMenuType.Image,
|
||||
position: action.position,
|
||||
},
|
||||
}
|
||||
}
|
||||
case OPEN_MARK_ALL_MENU: return {
|
||||
...state,
|
||||
contextMenu: {
|
||||
type: state.contextMenu.type === ContextMenuType.MarkRead
|
||||
? ContextMenuType.Hidden : ContextMenuType.MarkRead,
|
||||
event: "#mark-all-toggle"
|
||||
case OPEN_MARK_ALL_MENU:
|
||||
return {
|
||||
...state,
|
||||
contextMenu: {
|
||||
type:
|
||||
state.contextMenu.type === ContextMenuType.MarkRead
|
||||
? ContextMenuType.Hidden
|
||||
: ContextMenuType.MarkRead,
|
||||
event: "#mark-all-toggle",
|
||||
},
|
||||
}
|
||||
}
|
||||
case TOGGLE_MENU: return {
|
||||
...state,
|
||||
menu: !state.menu
|
||||
}
|
||||
case SAVE_SETTINGS: return {
|
||||
...state,
|
||||
settings: {
|
||||
...state.settings,
|
||||
display: true,
|
||||
changed: true,
|
||||
saving: !state.settings.saving
|
||||
case TOGGLE_MENU:
|
||||
return {
|
||||
...state,
|
||||
menu: !state.menu,
|
||||
}
|
||||
}
|
||||
case TOGGLE_SETTINGS: return {
|
||||
...state,
|
||||
settings: {
|
||||
display: action.open,
|
||||
changed: false,
|
||||
sids: action.sids,
|
||||
saving: false
|
||||
case SAVE_SETTINGS:
|
||||
return {
|
||||
...state,
|
||||
settings: {
|
||||
...state.settings,
|
||||
display: true,
|
||||
changed: true,
|
||||
saving: !state.settings.saving,
|
||||
},
|
||||
}
|
||||
}
|
||||
case TOGGLE_LOGS: return {
|
||||
...state,
|
||||
logMenu: {
|
||||
...state.logMenu,
|
||||
display: !state.logMenu.display,
|
||||
notify: false
|
||||
case TOGGLE_SETTINGS:
|
||||
return {
|
||||
...state,
|
||||
settings: {
|
||||
display: action.open,
|
||||
changed: false,
|
||||
sids: action.sids,
|
||||
saving: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
case PUSH_NOTIFICATION: return {
|
||||
...state,
|
||||
logMenu: {
|
||||
...state.logMenu,
|
||||
notify: true,
|
||||
logs: [
|
||||
...state.logMenu.logs,
|
||||
new AppLog(AppLogType.Article, action.title, action.source, action.iid)
|
||||
]
|
||||
case TOGGLE_LOGS:
|
||||
return {
|
||||
...state,
|
||||
logMenu: {
|
||||
...state.logMenu,
|
||||
display: !state.logMenu.display,
|
||||
notify: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
default: return state
|
||||
case PUSH_NOTIFICATION:
|
||||
return {
|
||||
...state,
|
||||
logMenu: {
|
||||
...state.logMenu,
|
||||
notify: true,
|
||||
logs: [
|
||||
...state.logMenu.logs,
|
||||
new AppLog(
|
||||
AppLogType.Article,
|
||||
action.title,
|
||||
action.source,
|
||||
action.iid
|
||||
),
|
||||
],
|
||||
},
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,18 @@
|
|||
import * as db from "../db"
|
||||
import lf from "lovefield"
|
||||
import { SourceActionTypes, INIT_SOURCES, ADD_SOURCE, DELETE_SOURCE } from "./source"
|
||||
import { ItemActionTypes, FETCH_ITEMS, RSSItem, TOGGLE_HIDDEN, applyItemReduction } from "./item"
|
||||
import {
|
||||
SourceActionTypes,
|
||||
INIT_SOURCES,
|
||||
ADD_SOURCE,
|
||||
DELETE_SOURCE,
|
||||
} from "./source"
|
||||
import {
|
||||
ItemActionTypes,
|
||||
FETCH_ITEMS,
|
||||
RSSItem,
|
||||
TOGGLE_HIDDEN,
|
||||
applyItemReduction,
|
||||
} from "./item"
|
||||
import { ActionStatus, AppThunk, mergeSortedArrays } from "../utils"
|
||||
import { PageActionTypes, SELECT_PAGE, PageType, APPLY_FILTER } from "./page"
|
||||
|
||||
|
@ -23,10 +34,13 @@ export class FeedFilter {
|
|||
type: FilterType
|
||||
search: string
|
||||
|
||||
constructor(type: FilterType = null, search="") {
|
||||
if (type === null && (type = window.settings.getFilterType()) === null) {
|
||||
constructor(type: FilterType = null, search = "") {
|
||||
if (
|
||||
type === null &&
|
||||
(type = window.settings.getFilterType()) === null
|
||||
) {
|
||||
type = FilterType.Default | FilterType.CaseInsensitive
|
||||
}
|
||||
}
|
||||
this.type = type
|
||||
this.search = search
|
||||
}
|
||||
|
@ -34,17 +48,22 @@ export class FeedFilter {
|
|||
static toPredicates(filter: FeedFilter) {
|
||||
let type = filter.type
|
||||
const predicates = new Array<lf.Predicate>()
|
||||
if (!(type & FilterType.ShowRead)) predicates.push(db.items.hasRead.eq(false))
|
||||
if (!(type & FilterType.ShowNotStarred)) predicates.push(db.items.starred.eq(true))
|
||||
if (!(type & FilterType.ShowHidden)) predicates.push(db.items.hidden.eq(false))
|
||||
if (!(type & FilterType.ShowRead))
|
||||
predicates.push(db.items.hasRead.eq(false))
|
||||
if (!(type & FilterType.ShowNotStarred))
|
||||
predicates.push(db.items.starred.eq(true))
|
||||
if (!(type & FilterType.ShowHidden))
|
||||
predicates.push(db.items.hidden.eq(false))
|
||||
if (filter.search !== "") {
|
||||
const flags = (type & FilterType.CaseInsensitive) ? "i" : ""
|
||||
const flags = type & FilterType.CaseInsensitive ? "i" : ""
|
||||
const regex = RegExp(filter.search, flags)
|
||||
if (type & FilterType.FullSearch) {
|
||||
predicates.push(lf.op.or(
|
||||
db.items.title.match(regex),
|
||||
db.items.snippet.match(regex)
|
||||
))
|
||||
predicates.push(
|
||||
lf.op.or(
|
||||
db.items.title.match(regex),
|
||||
db.items.snippet.match(regex)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
predicates.push(db.items.title.match(regex))
|
||||
}
|
||||
|
@ -58,13 +77,14 @@ export class FeedFilter {
|
|||
if (!(type & FilterType.ShowRead)) flag = flag && !item.hasRead
|
||||
if (!(type & FilterType.ShowNotStarred)) flag = flag && item.starred
|
||||
if (!(type & FilterType.ShowHidden)) flag = flag && !item.hidden
|
||||
if (filter.search !== "") {
|
||||
const flags = (type & FilterType.CaseInsensitive) ? "i" : ""
|
||||
if (filter.search !== "") {
|
||||
const flags = type & FilterType.CaseInsensitive ? "i" : ""
|
||||
const regex = RegExp(filter.search, flags)
|
||||
if (type & FilterType.FullSearch) {
|
||||
flag = flag && (regex.test(item.title) || regex.test(item.snippet))
|
||||
flag =
|
||||
flag && (regex.test(item.title) || regex.test(item.snippet))
|
||||
} else if (type & FilterType.CreatorSearch) {
|
||||
flag = flag && (regex.test(item.creator || ""))
|
||||
flag = flag && regex.test(item.creator || "")
|
||||
} else {
|
||||
flag = flag && regex.test(item.title)
|
||||
}
|
||||
|
@ -87,7 +107,7 @@ export class RSSFeed {
|
|||
iids: number[]
|
||||
filter: FeedFilter
|
||||
|
||||
constructor (id: string = null, sids=[], filter=null) {
|
||||
constructor(id: string = null, sids = [], filter = null) {
|
||||
this._id = id
|
||||
this.sids = sids
|
||||
this.iids = []
|
||||
|
@ -99,12 +119,14 @@ export class RSSFeed {
|
|||
static async loadFeed(feed: RSSFeed, skip = 0): Promise<RSSItem[]> {
|
||||
const predicates = FeedFilter.toPredicates(feed.filter)
|
||||
predicates.push(db.items.source.in(feed.sids))
|
||||
return (await db.itemsDB.select().from(db.items).where(
|
||||
lf.op.and.apply(null, predicates)
|
||||
).orderBy(db.items.date, lf.Order.DESC)
|
||||
.skip(skip)
|
||||
.limit(LOAD_QUANTITY)
|
||||
.exec()) as RSSItem[]
|
||||
return (await db.itemsDB
|
||||
.select()
|
||||
.from(db.items)
|
||||
.where(lf.op.and.apply(null, predicates))
|
||||
.orderBy(db.items.date, lf.Order.DESC)
|
||||
.skip(skip)
|
||||
.limit(LOAD_QUANTITY)
|
||||
.exec()) as RSSItem[]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -138,13 +160,16 @@ interface loadMoreAction {
|
|||
err?
|
||||
}
|
||||
|
||||
interface dismissItemsAction{
|
||||
interface dismissItemsAction {
|
||||
type: typeof DISMISS_ITEMS
|
||||
fid: string
|
||||
iids: Set<number>
|
||||
}
|
||||
|
||||
export type FeedActionTypes = initFeedAction | initFeedsAction | loadMoreAction
|
||||
export type FeedActionTypes =
|
||||
| initFeedAction
|
||||
| initFeedsAction
|
||||
| loadMoreAction
|
||||
| dismissItemsAction
|
||||
|
||||
export function dismissItems(): AppThunk {
|
||||
|
@ -162,7 +187,7 @@ export function dismissItems(): AppThunk {
|
|||
dispatch({
|
||||
type: DISMISS_ITEMS,
|
||||
fid: fid,
|
||||
iids: iids
|
||||
iids: iids,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -170,22 +195,25 @@ export function dismissItems(): AppThunk {
|
|||
export function initFeedsRequest(): FeedActionTypes {
|
||||
return {
|
||||
type: INIT_FEEDS,
|
||||
status: ActionStatus.Request
|
||||
status: ActionStatus.Request,
|
||||
}
|
||||
}
|
||||
export function initFeedsSuccess(): FeedActionTypes {
|
||||
return {
|
||||
type: INIT_FEEDS,
|
||||
status: ActionStatus.Success
|
||||
status: ActionStatus.Success,
|
||||
}
|
||||
}
|
||||
|
||||
export function initFeedSuccess(feed: RSSFeed, items: RSSItem[]): FeedActionTypes {
|
||||
export function initFeedSuccess(
|
||||
feed: RSSFeed,
|
||||
items: RSSItem[]
|
||||
): FeedActionTypes {
|
||||
return {
|
||||
type: INIT_FEED,
|
||||
status: ActionStatus.Success,
|
||||
items: items,
|
||||
feed: feed
|
||||
feed: feed,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -193,7 +221,7 @@ export function initFeedFailure(err): FeedActionTypes {
|
|||
return {
|
||||
type: INIT_FEED,
|
||||
status: ActionStatus.Failure,
|
||||
err: err
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -203,12 +231,14 @@ export function initFeeds(force = false): AppThunk<Promise<void>> {
|
|||
let promises = new Array<Promise<void>>()
|
||||
for (let feed of Object.values(getState().feeds)) {
|
||||
if (!feed.loaded || force) {
|
||||
let p = RSSFeed.loadFeed(feed).then(items => {
|
||||
dispatch(initFeedSuccess(feed, items))
|
||||
}).catch(err => {
|
||||
console.log(err)
|
||||
dispatch(initFeedFailure(err))
|
||||
})
|
||||
let p = RSSFeed.loadFeed(feed)
|
||||
.then(items => {
|
||||
dispatch(initFeedSuccess(feed, items))
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
dispatch(initFeedFailure(err))
|
||||
})
|
||||
promises.push(p)
|
||||
}
|
||||
}
|
||||
|
@ -222,16 +252,19 @@ export function loadMoreRequest(feed: RSSFeed): FeedActionTypes {
|
|||
return {
|
||||
type: LOAD_MORE,
|
||||
status: ActionStatus.Request,
|
||||
feed: feed
|
||||
feed: feed,
|
||||
}
|
||||
}
|
||||
|
||||
export function loadMoreSuccess(feed: RSSFeed, items: RSSItem[]): FeedActionTypes {
|
||||
export function loadMoreSuccess(
|
||||
feed: RSSFeed,
|
||||
items: RSSItem[]
|
||||
): FeedActionTypes {
|
||||
return {
|
||||
type: LOAD_MORE,
|
||||
status: ActionStatus.Success,
|
||||
feed: feed,
|
||||
items: items
|
||||
items: items,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -240,7 +273,7 @@ export function loadMoreFailure(feed: RSSFeed, err): FeedActionTypes {
|
|||
type: LOAD_MORE,
|
||||
status: ActionStatus.Failure,
|
||||
feed: feed,
|
||||
err: err
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -249,43 +282,68 @@ export function loadMore(feed: RSSFeed): AppThunk<Promise<void>> {
|
|||
if (feed.loaded && !feed.loading && !feed.allLoaded) {
|
||||
dispatch(loadMoreRequest(feed))
|
||||
const state = getState()
|
||||
const skipNum = feed.iids.filter(i => FeedFilter.testItem(feed.filter, state.items[i])).length
|
||||
return RSSFeed.loadFeed(feed, skipNum).then(items => {
|
||||
dispatch(loadMoreSuccess(feed, items))
|
||||
}).catch(e => {
|
||||
console.log(e)
|
||||
dispatch(loadMoreFailure(feed, e))
|
||||
})
|
||||
const skipNum = feed.iids.filter(i =>
|
||||
FeedFilter.testItem(feed.filter, state.items[i])
|
||||
).length
|
||||
return RSSFeed.loadFeed(feed, skipNum)
|
||||
.then(items => {
|
||||
dispatch(loadMoreSuccess(feed, items))
|
||||
})
|
||||
.catch(e => {
|
||||
console.log(e)
|
||||
dispatch(loadMoreFailure(feed, e))
|
||||
})
|
||||
}
|
||||
return new Promise((_, reject) => { reject() })
|
||||
return new Promise((_, reject) => {
|
||||
reject()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function feedReducer(
|
||||
state: FeedState = { [ALL]: new RSSFeed(ALL) },
|
||||
action: SourceActionTypes | ItemActionTypes | FeedActionTypes | PageActionTypes
|
||||
action:
|
||||
| SourceActionTypes
|
||||
| ItemActionTypes
|
||||
| FeedActionTypes
|
||||
| PageActionTypes
|
||||
): FeedState {
|
||||
switch (action.type) {
|
||||
case INIT_SOURCES:
|
||||
switch (action.status) {
|
||||
case ActionStatus.Success: return {
|
||||
...state,
|
||||
[ALL]: new RSSFeed(ALL, Object.values(action.sources).map(s => s.sid))
|
||||
}
|
||||
default: return state
|
||||
case ActionStatus.Success:
|
||||
return {
|
||||
...state,
|
||||
[ALL]: new RSSFeed(
|
||||
ALL,
|
||||
Object.values(action.sources).map(s => s.sid)
|
||||
),
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
case ADD_SOURCE:
|
||||
switch (action.status) {
|
||||
case ActionStatus.Success: return {
|
||||
...state,
|
||||
[ALL]: new RSSFeed(ALL, [...state[ALL].sids, action.source.sid], state[ALL].filter)
|
||||
}
|
||||
default: return state
|
||||
case ActionStatus.Success:
|
||||
return {
|
||||
...state,
|
||||
[ALL]: new RSSFeed(
|
||||
ALL,
|
||||
[...state[ALL].sids, action.source.sid],
|
||||
state[ALL].filter
|
||||
),
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
case DELETE_SOURCE: {
|
||||
let nextState = {}
|
||||
for (let [id, feed] of Object.entries(state)) {
|
||||
nextState[id] = new RSSFeed(id, feed.sids.filter(sid => sid != action.source.sid), feed.filter)
|
||||
nextState[id] = new RSSFeed(
|
||||
id,
|
||||
feed.sids.filter(sid => sid != action.source.sid),
|
||||
feed.filter
|
||||
)
|
||||
}
|
||||
return nextState
|
||||
}
|
||||
|
@ -294,7 +352,7 @@ export function feedReducer(
|
|||
for (let [id, feed] of Object.entries(state)) {
|
||||
nextState[id] = {
|
||||
...feed,
|
||||
filter: action.filter
|
||||
filter: action.filter,
|
||||
}
|
||||
}
|
||||
return nextState
|
||||
|
@ -305,79 +363,102 @@ export function feedReducer(
|
|||
let nextState = { ...state }
|
||||
for (let feed of Object.values(state)) {
|
||||
if (feed.loaded) {
|
||||
let items = action.items
|
||||
.filter(i => feed.sids.includes(i.source) && FeedFilter.testItem(feed.filter, i))
|
||||
let items = action.items.filter(
|
||||
i =>
|
||||
feed.sids.includes(i.source) &&
|
||||
FeedFilter.testItem(feed.filter, i)
|
||||
)
|
||||
if (items.length > 0) {
|
||||
let oldItems = feed.iids.map(id => action.itemState[id])
|
||||
let nextItems = mergeSortedArrays(oldItems, items, (a, b) => b.date.getTime() - a.date.getTime())
|
||||
nextState[feed._id] = {
|
||||
...feed,
|
||||
iids: nextItems.map(i => i._id)
|
||||
let oldItems = feed.iids.map(
|
||||
id => action.itemState[id]
|
||||
)
|
||||
let nextItems = mergeSortedArrays(
|
||||
oldItems,
|
||||
items,
|
||||
(a, b) =>
|
||||
b.date.getTime() - a.date.getTime()
|
||||
)
|
||||
nextState[feed._id] = {
|
||||
...feed,
|
||||
iids: nextItems.map(i => i._id),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nextState
|
||||
}
|
||||
default: return state
|
||||
default:
|
||||
return state
|
||||
}
|
||||
case DISMISS_ITEMS:
|
||||
let nextState = { ...state }
|
||||
let feed = state[action.fid]
|
||||
nextState[action.fid] = {
|
||||
...feed,
|
||||
iids: feed.iids.filter(iid => !action.iids.has(iid))
|
||||
iids: feed.iids.filter(iid => !action.iids.has(iid)),
|
||||
}
|
||||
return nextState
|
||||
case INIT_FEED:
|
||||
case INIT_FEED:
|
||||
switch (action.status) {
|
||||
case ActionStatus.Success: return {
|
||||
...state,
|
||||
[action.feed._id]: {
|
||||
...action.feed,
|
||||
loaded: true,
|
||||
allLoaded: action.items.length < LOAD_QUANTITY,
|
||||
iids: action.items.map(i => i._id)
|
||||
case ActionStatus.Success:
|
||||
return {
|
||||
...state,
|
||||
[action.feed._id]: {
|
||||
...action.feed,
|
||||
loaded: true,
|
||||
allLoaded: action.items.length < LOAD_QUANTITY,
|
||||
iids: action.items.map(i => i._id),
|
||||
},
|
||||
}
|
||||
}
|
||||
default: return state
|
||||
default:
|
||||
return state
|
||||
}
|
||||
case LOAD_MORE:
|
||||
switch (action.status) {
|
||||
case ActionStatus.Request: return {
|
||||
...state,
|
||||
[action.feed._id] : {
|
||||
...action.feed,
|
||||
loading: true
|
||||
case ActionStatus.Request:
|
||||
return {
|
||||
...state,
|
||||
[action.feed._id]: {
|
||||
...action.feed,
|
||||
loading: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
case ActionStatus.Success: return {
|
||||
...state,
|
||||
[action.feed._id] : {
|
||||
...action.feed,
|
||||
loading: false,
|
||||
allLoaded: action.items.length < LOAD_QUANTITY,
|
||||
iids: [...action.feed.iids, ...action.items.map(i => i._id)]
|
||||
case ActionStatus.Success:
|
||||
return {
|
||||
...state,
|
||||
[action.feed._id]: {
|
||||
...action.feed,
|
||||
loading: false,
|
||||
allLoaded: action.items.length < LOAD_QUANTITY,
|
||||
iids: [
|
||||
...action.feed.iids,
|
||||
...action.items.map(i => i._id),
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
case ActionStatus.Failure: return {
|
||||
...state,
|
||||
[action.feed._id] : {
|
||||
...action.feed,
|
||||
loading: false
|
||||
case ActionStatus.Failure:
|
||||
return {
|
||||
...state,
|
||||
[action.feed._id]: {
|
||||
...action.feed,
|
||||
loading: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
default: return state
|
||||
default:
|
||||
return state
|
||||
}
|
||||
case TOGGLE_HIDDEN: {
|
||||
let nextItem = applyItemReduction(action.item, action.type)
|
||||
let filteredFeeds = Object.values(state).filter(feed => feed.loaded && !FeedFilter.testItem(feed.filter, nextItem))
|
||||
let filteredFeeds = Object.values(state).filter(
|
||||
feed =>
|
||||
feed.loaded && !FeedFilter.testItem(feed.filter, nextItem)
|
||||
)
|
||||
if (filteredFeeds.length > 0) {
|
||||
let nextState = { ...state }
|
||||
for (let feed of filteredFeeds) {
|
||||
nextState[feed._id] = {
|
||||
...feed,
|
||||
iids: feed.iids.filter(id => id != nextItem._id)
|
||||
iids: feed.iids.filter(id => id != nextItem._id),
|
||||
}
|
||||
}
|
||||
return nextState
|
||||
|
@ -387,20 +468,30 @@ export function feedReducer(
|
|||
}
|
||||
case SELECT_PAGE:
|
||||
switch (action.pageType) {
|
||||
case PageType.Sources: return {
|
||||
...state,
|
||||
[SOURCE]: new RSSFeed(SOURCE, action.sids, action.filter)
|
||||
}
|
||||
case PageType.AllArticles: return action.init ? {
|
||||
...state,
|
||||
[ALL]: {
|
||||
...state[ALL],
|
||||
loaded: false,
|
||||
filter: action.filter
|
||||
case PageType.Sources:
|
||||
return {
|
||||
...state,
|
||||
[SOURCE]: new RSSFeed(
|
||||
SOURCE,
|
||||
action.sids,
|
||||
action.filter
|
||||
),
|
||||
}
|
||||
} : state
|
||||
default: return state
|
||||
case PageType.AllArticles:
|
||||
return action.init
|
||||
? {
|
||||
...state,
|
||||
[ALL]: {
|
||||
...state[ALL],
|
||||
loaded: false,
|
||||
filter: action.filter,
|
||||
},
|
||||
}
|
||||
: state
|
||||
default:
|
||||
return state
|
||||
}
|
||||
default: return state
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,20 @@
|
|||
import intl from "react-intl-universal"
|
||||
import { SourceActionTypes, ADD_SOURCE, DELETE_SOURCE, addSource, RSSSource, SourceState } from "./source"
|
||||
import {
|
||||
SourceActionTypes,
|
||||
ADD_SOURCE,
|
||||
DELETE_SOURCE,
|
||||
addSource,
|
||||
RSSSource,
|
||||
SourceState,
|
||||
} from "./source"
|
||||
import { SourceGroup } from "../../schema-types"
|
||||
import { ActionStatus, AppThunk, domParser } from "../utils"
|
||||
import { saveSettings } from "./app"
|
||||
import { fetchItemsIntermediate, fetchItemsRequest, fetchItemsSuccess } from "./item"
|
||||
import {
|
||||
fetchItemsIntermediate,
|
||||
fetchItemsRequest,
|
||||
fetchItemsSuccess,
|
||||
} from "./item"
|
||||
|
||||
export const CREATE_SOURCE_GROUP = "CREATE_SOURCE_GROUP"
|
||||
export const ADD_SOURCE_TO_GROUP = "ADD_SOURCE_TO_GROUP"
|
||||
|
@ -14,51 +25,58 @@ export const DELETE_SOURCE_GROUP = "DELETE_SOURCE_GROUP"
|
|||
export const TOGGLE_GROUP_EXPANSION = "TOGGLE_GROUP_EXPANSION"
|
||||
|
||||
interface CreateSourceGroupAction {
|
||||
type: typeof CREATE_SOURCE_GROUP,
|
||||
type: typeof CREATE_SOURCE_GROUP
|
||||
group: SourceGroup
|
||||
}
|
||||
|
||||
interface AddSourceToGroupAction {
|
||||
type: typeof ADD_SOURCE_TO_GROUP,
|
||||
groupIndex: number,
|
||||
type: typeof ADD_SOURCE_TO_GROUP
|
||||
groupIndex: number
|
||||
sid: number
|
||||
}
|
||||
|
||||
interface RemoveSourceFromGroupAction {
|
||||
type: typeof REMOVE_SOURCE_FROM_GROUP,
|
||||
groupIndex: number,
|
||||
type: typeof REMOVE_SOURCE_FROM_GROUP
|
||||
groupIndex: number
|
||||
sids: number[]
|
||||
}
|
||||
|
||||
interface UpdateSourceGroupAction {
|
||||
type: typeof UPDATE_SOURCE_GROUP,
|
||||
groupIndex: number,
|
||||
type: typeof UPDATE_SOURCE_GROUP
|
||||
groupIndex: number
|
||||
group: SourceGroup
|
||||
}
|
||||
|
||||
interface ReorderSourceGroupsAction {
|
||||
type: typeof REORDER_SOURCE_GROUPS,
|
||||
type: typeof REORDER_SOURCE_GROUPS
|
||||
groups: SourceGroup[]
|
||||
}
|
||||
|
||||
interface DeleteSourceGroupAction {
|
||||
type: typeof DELETE_SOURCE_GROUP,
|
||||
type: typeof DELETE_SOURCE_GROUP
|
||||
groupIndex: number
|
||||
}
|
||||
|
||||
interface ToggleGroupExpansionAction {
|
||||
type: typeof TOGGLE_GROUP_EXPANSION,
|
||||
type: typeof TOGGLE_GROUP_EXPANSION
|
||||
groupIndex: number
|
||||
}
|
||||
|
||||
export type SourceGroupActionTypes = CreateSourceGroupAction | AddSourceToGroupAction
|
||||
| RemoveSourceFromGroupAction | UpdateSourceGroupAction | ReorderSourceGroupsAction
|
||||
| DeleteSourceGroupAction | ToggleGroupExpansionAction
|
||||
export type SourceGroupActionTypes =
|
||||
| CreateSourceGroupAction
|
||||
| AddSourceToGroupAction
|
||||
| RemoveSourceFromGroupAction
|
||||
| UpdateSourceGroupAction
|
||||
| ReorderSourceGroupsAction
|
||||
| DeleteSourceGroupAction
|
||||
| ToggleGroupExpansionAction
|
||||
|
||||
export function createSourceGroupDone(group: SourceGroup): SourceGroupActionTypes {
|
||||
export function createSourceGroupDone(
|
||||
group: SourceGroup
|
||||
): SourceGroupActionTypes {
|
||||
return {
|
||||
type: CREATE_SOURCE_GROUP,
|
||||
group: group
|
||||
group: group,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,11 +97,14 @@ export function createSourceGroup(name: string): AppThunk<number> {
|
|||
}
|
||||
}
|
||||
|
||||
function addSourceToGroupDone(groupIndex: number, sid: number): SourceGroupActionTypes {
|
||||
function addSourceToGroupDone(
|
||||
groupIndex: number,
|
||||
sid: number
|
||||
): SourceGroupActionTypes {
|
||||
return {
|
||||
type: ADD_SOURCE_TO_GROUP,
|
||||
groupIndex: groupIndex,
|
||||
sid: sid
|
||||
sid: sid,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -94,15 +115,21 @@ export function addSourceToGroup(groupIndex: number, sid: number): AppThunk {
|
|||
}
|
||||
}
|
||||
|
||||
function removeSourceFromGroupDone(groupIndex: number, sids: number[]): SourceGroupActionTypes {
|
||||
function removeSourceFromGroupDone(
|
||||
groupIndex: number,
|
||||
sids: number[]
|
||||
): SourceGroupActionTypes {
|
||||
return {
|
||||
type: REMOVE_SOURCE_FROM_GROUP,
|
||||
groupIndex: groupIndex,
|
||||
sids: sids
|
||||
sids: sids,
|
||||
}
|
||||
}
|
||||
|
||||
export function removeSourceFromGroup(groupIndex: number, sids: number[]): AppThunk {
|
||||
export function removeSourceFromGroup(
|
||||
groupIndex: number,
|
||||
sids: number[]
|
||||
): AppThunk {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(removeSourceFromGroupDone(groupIndex, sids))
|
||||
window.settings.saveGroups(getState().groups)
|
||||
|
@ -112,7 +139,7 @@ export function removeSourceFromGroup(groupIndex: number, sids: number[]): AppTh
|
|||
function deleteSourceGroupDone(groupIndex: number): SourceGroupActionTypes {
|
||||
return {
|
||||
type: DELETE_SOURCE_GROUP,
|
||||
groupIndex: groupIndex
|
||||
groupIndex: groupIndex,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,7 +154,7 @@ function updateSourceGroupDone(group: SourceGroup): SourceGroupActionTypes {
|
|||
return {
|
||||
type: UPDATE_SOURCE_GROUP,
|
||||
groupIndex: group.index,
|
||||
group: group
|
||||
group: group,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -138,10 +165,12 @@ export function updateSourceGroup(group: SourceGroup): AppThunk {
|
|||
}
|
||||
}
|
||||
|
||||
function reorderSourceGroupsDone(groups: SourceGroup[]): SourceGroupActionTypes {
|
||||
function reorderSourceGroupsDone(
|
||||
groups: SourceGroup[]
|
||||
): SourceGroupActionTypes {
|
||||
return {
|
||||
type: REORDER_SOURCE_GROUPS,
|
||||
groups: groups
|
||||
groups: groups,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -156,7 +185,7 @@ export function toggleGroupExpansion(groupIndex: number): AppThunk {
|
|||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: TOGGLE_GROUP_EXPANSION,
|
||||
groupIndex: groupIndex
|
||||
groupIndex: groupIndex,
|
||||
})
|
||||
window.settings.saveGroups(getState().groups)
|
||||
}
|
||||
|
@ -167,16 +196,18 @@ export function fixBrokenGroups(sources: SourceState): AppThunk {
|
|||
const { groups } = getState()
|
||||
const sids = new Set(Object.values(sources).map(s => s.sid))
|
||||
let isBroken = false
|
||||
const newGroups: SourceGroup[] = groups.map(group => {
|
||||
const newGroup: SourceGroup = {
|
||||
...group,
|
||||
sids: group.sids.filter(sid => sids.delete(sid))
|
||||
}
|
||||
if (newGroup.sids.length !== group.sids.length) {
|
||||
isBroken = true
|
||||
}
|
||||
return newGroup
|
||||
}).filter(group => group.isMultiple || group.sids.length > 0)
|
||||
const newGroups: SourceGroup[] = groups
|
||||
.map(group => {
|
||||
const newGroup: SourceGroup = {
|
||||
...group,
|
||||
sids: group.sids.filter(sid => sids.delete(sid)),
|
||||
}
|
||||
if (newGroup.sids.length !== group.sids.length) {
|
||||
isBroken = true
|
||||
}
|
||||
return newGroup
|
||||
})
|
||||
.filter(group => group.isMultiple || group.sids.length > 0)
|
||||
if (isBroken || sids.size > 0) {
|
||||
for (let sid of sids) {
|
||||
newGroups.push(new SourceGroup([sid]))
|
||||
|
@ -186,7 +217,9 @@ export function fixBrokenGroups(sources: SourceState): AppThunk {
|
|||
}
|
||||
}
|
||||
|
||||
function outlineToSource(outline: Element): [ReturnType<typeof addSource>, string] {
|
||||
function outlineToSource(
|
||||
outline: Element
|
||||
): [ReturnType<typeof addSource>, string] {
|
||||
let url = outline.getAttribute("xmlUrl")
|
||||
let name = outline.getAttribute("text") || outline.getAttribute("title")
|
||||
if (url) {
|
||||
|
@ -197,12 +230,16 @@ function outlineToSource(outline: Element): [ReturnType<typeof addSource>, strin
|
|||
}
|
||||
|
||||
export function importOPML(): AppThunk {
|
||||
return async (dispatch) => {
|
||||
const filters = [{ name: intl.get("sources.opmlFile"), extensions: ["xml", "opml"] }]
|
||||
return async dispatch => {
|
||||
const filters = [
|
||||
{ name: intl.get("sources.opmlFile"), extensions: ["xml", "opml"] },
|
||||
]
|
||||
window.utils.showOpenDialog(filters).then(data => {
|
||||
if (data) {
|
||||
dispatch(saveSettings())
|
||||
let doc = domParser.parseFromString(data, "text/xml").getElementsByTagName("body")
|
||||
let doc = domParser
|
||||
.parseFromString(data, "text/xml")
|
||||
.getElementsByTagName("body")
|
||||
if (doc.length == 0) {
|
||||
dispatch(saveSettings())
|
||||
return
|
||||
|
@ -210,43 +247,60 @@ export function importOPML(): AppThunk {
|
|||
let parseError = doc[0].getElementsByTagName("parsererror")
|
||||
if (parseError.length > 0) {
|
||||
dispatch(saveSettings())
|
||||
window.utils.showErrorBox(intl.get("sources.errorParse"), intl.get("sources.errorParseHint"))
|
||||
window.utils.showErrorBox(
|
||||
intl.get("sources.errorParse"),
|
||||
intl.get("sources.errorParseHint")
|
||||
)
|
||||
return
|
||||
}
|
||||
let sources: [ReturnType<typeof addSource>, number, string][] = []
|
||||
let sources: [ReturnType<typeof addSource>, number, string][] =
|
||||
[]
|
||||
let errors: [string, any][] = []
|
||||
for (let el of doc[0].children) {
|
||||
if (el.getAttribute("type") === "rss") {
|
||||
let source = outlineToSource(el)
|
||||
if (source) sources.push([source[0], -1, source[1]])
|
||||
} else if (el.hasAttribute("text") || el.hasAttribute("title")) {
|
||||
let groupName = el.getAttribute("text") || el.getAttribute("title")
|
||||
} else if (
|
||||
el.hasAttribute("text") ||
|
||||
el.hasAttribute("title")
|
||||
) {
|
||||
let groupName =
|
||||
el.getAttribute("text") || el.getAttribute("title")
|
||||
let gid = dispatch(createSourceGroup(groupName))
|
||||
for (let child of el.children) {
|
||||
let source = outlineToSource(child)
|
||||
if (source) sources.push([source[0], gid, source[1]])
|
||||
if (source)
|
||||
sources.push([source[0], gid, source[1]])
|
||||
}
|
||||
}
|
||||
}
|
||||
dispatch(fetchItemsRequest(sources.length))
|
||||
let promises = sources.map(([s, gid, url]) => {
|
||||
return dispatch(s).then(sid => {
|
||||
if (sid !== null && gid > -1) dispatch(addSourceToGroup(gid, sid))
|
||||
}).catch(err => {
|
||||
errors.push([url, err])
|
||||
}).finally(() => {
|
||||
dispatch(fetchItemsIntermediate())
|
||||
})
|
||||
return dispatch(s)
|
||||
.then(sid => {
|
||||
if (sid !== null && gid > -1)
|
||||
dispatch(addSourceToGroup(gid, sid))
|
||||
})
|
||||
.catch(err => {
|
||||
errors.push([url, err])
|
||||
})
|
||||
.finally(() => {
|
||||
dispatch(fetchItemsIntermediate())
|
||||
})
|
||||
})
|
||||
Promise.allSettled(promises).then(() => {
|
||||
dispatch(fetchItemsSuccess([], {}))
|
||||
dispatch(saveSettings())
|
||||
if (errors.length > 0) {
|
||||
window.utils.showErrorBox(
|
||||
intl.get("sources.errorImport", { count: errors.length }),
|
||||
errors.map(e => {
|
||||
return e[0] + "\n" + String(e[1])
|
||||
}).join("\n")
|
||||
intl.get("sources.errorImport", {
|
||||
count: errors.length,
|
||||
}),
|
||||
errors
|
||||
.map(e => {
|
||||
return e[0] + "\n" + String(e[1])
|
||||
})
|
||||
.join("\n")
|
||||
)
|
||||
}
|
||||
})
|
||||
|
@ -266,32 +320,46 @@ function sourceToOutline(source: RSSSource, xml: Document) {
|
|||
|
||||
export function exportOPML(): AppThunk {
|
||||
return (_, getState) => {
|
||||
const filters = [{ name: intl.get("sources.opmlFile"), extensions: ["opml"] }]
|
||||
window.utils.showSaveDialog(filters, "*/Fluent_Reader_Export.opml").then(write => {
|
||||
if (write) {
|
||||
let state = getState()
|
||||
let xml = domParser.parseFromString(
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><opml version=\"1.0\"><head><title>Fluent Reader Export</title></head><body></body></opml>",
|
||||
"text/xml"
|
||||
)
|
||||
let body = xml.getElementsByTagName("body")[0]
|
||||
for (let group of state.groups) {
|
||||
if (group.isMultiple) {
|
||||
let outline = xml.createElement("outline")
|
||||
outline.setAttribute("text", group.name)
|
||||
outline.setAttribute("title", group.name)
|
||||
for (let sid of group.sids) {
|
||||
outline.appendChild(sourceToOutline(state.sources[sid], xml))
|
||||
const filters = [
|
||||
{ name: intl.get("sources.opmlFile"), extensions: ["opml"] },
|
||||
]
|
||||
window.utils
|
||||
.showSaveDialog(filters, "*/Fluent_Reader_Export.opml")
|
||||
.then(write => {
|
||||
if (write) {
|
||||
let state = getState()
|
||||
let xml = domParser.parseFromString(
|
||||
'<?xml version="1.0" encoding="UTF-8"?><opml version="1.0"><head><title>Fluent Reader Export</title></head><body></body></opml>',
|
||||
"text/xml"
|
||||
)
|
||||
let body = xml.getElementsByTagName("body")[0]
|
||||
for (let group of state.groups) {
|
||||
if (group.isMultiple) {
|
||||
let outline = xml.createElement("outline")
|
||||
outline.setAttribute("text", group.name)
|
||||
outline.setAttribute("title", group.name)
|
||||
for (let sid of group.sids) {
|
||||
outline.appendChild(
|
||||
sourceToOutline(state.sources[sid], xml)
|
||||
)
|
||||
}
|
||||
body.appendChild(outline)
|
||||
} else {
|
||||
body.appendChild(
|
||||
sourceToOutline(
|
||||
state.sources[group.sids[0]],
|
||||
xml
|
||||
)
|
||||
)
|
||||
}
|
||||
body.appendChild(outline)
|
||||
} else {
|
||||
body.appendChild(sourceToOutline(state.sources[group.sids[0]], xml))
|
||||
}
|
||||
let serializer = new XMLSerializer()
|
||||
write(
|
||||
serializer.serializeToString(xml),
|
||||
intl.get("settings.writeError")
|
||||
)
|
||||
}
|
||||
let serializer = new XMLSerializer()
|
||||
write(serializer.serializeToString(xml), intl.get("settings.writeError"))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -301,52 +369,78 @@ export function groupReducer(
|
|||
state = window.settings.loadGroups(),
|
||||
action: SourceActionTypes | SourceGroupActionTypes
|
||||
): GroupState {
|
||||
switch(action.type) {
|
||||
switch (action.type) {
|
||||
case ADD_SOURCE:
|
||||
switch (action.status) {
|
||||
case ActionStatus.Success: return [
|
||||
...state,
|
||||
new SourceGroup([action.source.sid])
|
||||
]
|
||||
default: return state
|
||||
case ActionStatus.Success:
|
||||
return [...state, new SourceGroup([action.source.sid])]
|
||||
default:
|
||||
return state
|
||||
}
|
||||
case DELETE_SOURCE: return [
|
||||
...state.map(group => ({
|
||||
...group,
|
||||
sids: group.sids.filter(sid => sid != action.source.sid)
|
||||
})).filter(g => g.isMultiple || g.sids.length == 1)
|
||||
]
|
||||
case CREATE_SOURCE_GROUP: return [ ...state, action.group ]
|
||||
case ADD_SOURCE_TO_GROUP: return state.map((g, i) => ({
|
||||
...g,
|
||||
sids: i == action.groupIndex
|
||||
? [ ...g.sids.filter(sid => sid !== action.sid), action.sid ]
|
||||
: g.sids.filter(sid => sid !== action.sid)
|
||||
})).filter(g => g.isMultiple || g.sids.length > 0)
|
||||
case REMOVE_SOURCE_FROM_GROUP: return [
|
||||
...state.slice(0, action.groupIndex),
|
||||
{
|
||||
...state[action.groupIndex],
|
||||
sids: state[action.groupIndex].sids.filter(sid => !action.sids.includes(sid))
|
||||
},
|
||||
...action.sids.map(sid => new SourceGroup([sid])),
|
||||
...state.slice(action.groupIndex + 1)
|
||||
]
|
||||
case UPDATE_SOURCE_GROUP: return [
|
||||
...state.slice(0, action.groupIndex),
|
||||
action.group,
|
||||
...state.slice(action.groupIndex + 1)
|
||||
]
|
||||
case REORDER_SOURCE_GROUPS: return action.groups
|
||||
case DELETE_SOURCE_GROUP: return [
|
||||
...state.slice(0, action.groupIndex),
|
||||
...state[action.groupIndex].sids.map(sid => new SourceGroup([sid])),
|
||||
...state.slice(action.groupIndex + 1)
|
||||
]
|
||||
case TOGGLE_GROUP_EXPANSION: return state.map((g, i) => i == action.groupIndex ? ({
|
||||
...g,
|
||||
expanded: !g.expanded
|
||||
}) : g)
|
||||
default: return state
|
||||
case DELETE_SOURCE:
|
||||
return [
|
||||
...state
|
||||
.map(group => ({
|
||||
...group,
|
||||
sids: group.sids.filter(
|
||||
sid => sid != action.source.sid
|
||||
),
|
||||
}))
|
||||
.filter(g => g.isMultiple || g.sids.length == 1),
|
||||
]
|
||||
case CREATE_SOURCE_GROUP:
|
||||
return [...state, action.group]
|
||||
case ADD_SOURCE_TO_GROUP:
|
||||
return state
|
||||
.map((g, i) => ({
|
||||
...g,
|
||||
sids:
|
||||
i == action.groupIndex
|
||||
? [
|
||||
...g.sids.filter(sid => sid !== action.sid),
|
||||
action.sid,
|
||||
]
|
||||
: g.sids.filter(sid => sid !== action.sid),
|
||||
}))
|
||||
.filter(g => g.isMultiple || g.sids.length > 0)
|
||||
case REMOVE_SOURCE_FROM_GROUP:
|
||||
return [
|
||||
...state.slice(0, action.groupIndex),
|
||||
{
|
||||
...state[action.groupIndex],
|
||||
sids: state[action.groupIndex].sids.filter(
|
||||
sid => !action.sids.includes(sid)
|
||||
),
|
||||
},
|
||||
...action.sids.map(sid => new SourceGroup([sid])),
|
||||
...state.slice(action.groupIndex + 1),
|
||||
]
|
||||
case UPDATE_SOURCE_GROUP:
|
||||
return [
|
||||
...state.slice(0, action.groupIndex),
|
||||
action.group,
|
||||
...state.slice(action.groupIndex + 1),
|
||||
]
|
||||
case REORDER_SOURCE_GROUPS:
|
||||
return action.groups
|
||||
case DELETE_SOURCE_GROUP:
|
||||
return [
|
||||
...state.slice(0, action.groupIndex),
|
||||
...state[action.groupIndex].sids.map(
|
||||
sid => new SourceGroup([sid])
|
||||
),
|
||||
...state.slice(action.groupIndex + 1),
|
||||
]
|
||||
case TOGGLE_GROUP_EXPANSION:
|
||||
return state.map((g, i) =>
|
||||
i == action.groupIndex
|
||||
? {
|
||||
...g,
|
||||
expanded: !g.expanded,
|
||||
}
|
||||
: g
|
||||
)
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,35 @@
|
|||
import * as db from "../db"
|
||||
import lf from "lovefield"
|
||||
import intl from "react-intl-universal"
|
||||
import { domParser, htmlDecode, ActionStatus, AppThunk, platformCtrl } from "../utils"
|
||||
import {
|
||||
domParser,
|
||||
htmlDecode,
|
||||
ActionStatus,
|
||||
AppThunk,
|
||||
platformCtrl,
|
||||
} from "../utils"
|
||||
import { RSSSource, updateSource, updateUnreadCounts } from "./source"
|
||||
import { FeedActionTypes, INIT_FEED, LOAD_MORE, FilterType, initFeeds, dismissItems } from "./feed"
|
||||
import {
|
||||
FeedActionTypes,
|
||||
INIT_FEED,
|
||||
LOAD_MORE,
|
||||
FilterType,
|
||||
initFeeds,
|
||||
dismissItems,
|
||||
} from "./feed"
|
||||
import Parser from "@yang991178/rss-parser"
|
||||
import { pushNotification, setupAutoFetch, SettingsActionTypes, FREE_MEMORY } from "./app"
|
||||
import { getServiceHooks, syncWithService, ServiceActionTypes, SYNC_LOCAL_ITEMS } from "./service"
|
||||
import {
|
||||
pushNotification,
|
||||
setupAutoFetch,
|
||||
SettingsActionTypes,
|
||||
FREE_MEMORY,
|
||||
} from "./app"
|
||||
import {
|
||||
getServiceHooks,
|
||||
syncWithService,
|
||||
ServiceActionTypes,
|
||||
SYNC_LOCAL_ITEMS,
|
||||
} from "./service"
|
||||
|
||||
export class RSSItem {
|
||||
_id: number
|
||||
|
@ -25,7 +48,7 @@ export class RSSItem {
|
|||
notify: boolean
|
||||
serviceRef?: string
|
||||
|
||||
constructor (item: Parser.Item, source: RSSSource) {
|
||||
constructor(item: Parser.Item, source: RSSSource) {
|
||||
for (let field of ["title", "link", "creator"]) {
|
||||
const content = item[field]
|
||||
if (content && typeof content !== "string") delete item[field]
|
||||
|
@ -54,25 +77,34 @@ export class RSSItem {
|
|||
item.content = parsed.content || ""
|
||||
item.snippet = htmlDecode(parsed.contentSnippet || "")
|
||||
}
|
||||
if (parsed.thumb) {
|
||||
if (parsed.thumb) {
|
||||
item.thumb = parsed.thumb
|
||||
} else if (parsed.image && parsed.image.$ && parsed.image.$.url) {
|
||||
item.thumb = parsed.image.$.url
|
||||
} else if (parsed.image && typeof parsed.image === "string") {
|
||||
item.thumb = parsed.image
|
||||
} else if (parsed.mediaContent) {
|
||||
let images = parsed.mediaContent.filter(c => c.$ && c.$.medium === "image" && c.$.url)
|
||||
let images = parsed.mediaContent.filter(
|
||||
c => c.$ && c.$.medium === "image" && c.$.url
|
||||
)
|
||||
if (images.length > 0) item.thumb = images[0].$.url
|
||||
}
|
||||
if (!item.thumb) {
|
||||
let dom = domParser.parseFromString(item.content, "text/html")
|
||||
let baseEl = dom.createElement('base')
|
||||
baseEl.setAttribute('href', item.link.split("/").slice(0, 3).join("/"))
|
||||
let baseEl = dom.createElement("base")
|
||||
baseEl.setAttribute(
|
||||
"href",
|
||||
item.link.split("/").slice(0, 3).join("/")
|
||||
)
|
||||
dom.head.append(baseEl)
|
||||
let img = dom.querySelector("img")
|
||||
if (img && img.src) item.thumb = img.src
|
||||
}
|
||||
if (item.thumb && !item.thumb.startsWith("https://") && !item.thumb.startsWith("http://")) {
|
||||
if (
|
||||
item.thumb &&
|
||||
!item.thumb.startsWith("https://") &&
|
||||
!item.thumb.startsWith("http://")
|
||||
) {
|
||||
delete item.thumb
|
||||
}
|
||||
}
|
||||
|
@ -105,7 +137,7 @@ interface MarkReadAction {
|
|||
}
|
||||
|
||||
interface MarkAllReadAction {
|
||||
type: typeof MARK_ALL_READ,
|
||||
type: typeof MARK_ALL_READ
|
||||
sids: number[]
|
||||
time?: number
|
||||
before?: boolean
|
||||
|
@ -126,23 +158,31 @@ interface ToggleHiddenAction {
|
|||
item: RSSItem
|
||||
}
|
||||
|
||||
export type ItemActionTypes = FetchItemsAction | MarkReadAction | MarkAllReadAction | MarkUnreadAction
|
||||
| ToggleStarredAction | ToggleHiddenAction
|
||||
export type ItemActionTypes =
|
||||
| FetchItemsAction
|
||||
| MarkReadAction
|
||||
| MarkAllReadAction
|
||||
| MarkUnreadAction
|
||||
| ToggleStarredAction
|
||||
| ToggleHiddenAction
|
||||
|
||||
export function fetchItemsRequest(fetchCount = 0): ItemActionTypes {
|
||||
return {
|
||||
type: FETCH_ITEMS,
|
||||
status: ActionStatus.Request,
|
||||
fetchCount: fetchCount
|
||||
fetchCount: fetchCount,
|
||||
}
|
||||
}
|
||||
|
||||
export function fetchItemsSuccess(items: RSSItem[], itemState: ItemState): ItemActionTypes {
|
||||
export function fetchItemsSuccess(
|
||||
items: RSSItem[],
|
||||
itemState: ItemState
|
||||
): ItemActionTypes {
|
||||
return {
|
||||
type: FETCH_ITEMS,
|
||||
status: ActionStatus.Success,
|
||||
items: items,
|
||||
itemState: itemState
|
||||
itemState: itemState,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -151,41 +191,65 @@ export function fetchItemsFailure(source: RSSSource, err): ItemActionTypes {
|
|||
type: FETCH_ITEMS,
|
||||
status: ActionStatus.Failure,
|
||||
errSource: source,
|
||||
err: err
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
export function fetchItemsIntermediate(): ItemActionTypes {
|
||||
return {
|
||||
type: FETCH_ITEMS,
|
||||
status: ActionStatus.Intermediate
|
||||
status: ActionStatus.Intermediate,
|
||||
}
|
||||
}
|
||||
|
||||
export async function insertItems(items: RSSItem[]): Promise<RSSItem[]> {
|
||||
items.sort((a, b) => a.date.getTime() - b.date.getTime())
|
||||
const rows = items.map(item => db.items.createRow(item))
|
||||
return (await db.itemsDB.insert().into(db.items).values(rows).exec()) as RSSItem[]
|
||||
return (await db.itemsDB
|
||||
.insert()
|
||||
.into(db.items)
|
||||
.values(rows)
|
||||
.exec()) as RSSItem[]
|
||||
}
|
||||
|
||||
export function fetchItems(background = false, sids: number[] = null): AppThunk<Promise<void>> {
|
||||
export function fetchItems(
|
||||
background = false,
|
||||
sids: number[] = null
|
||||
): AppThunk<Promise<void>> {
|
||||
return async (dispatch, getState) => {
|
||||
let promises = new Array<Promise<RSSItem[]>>()
|
||||
const initState = getState()
|
||||
if (!initState.app.fetchingItems && !initState.app.syncing) {
|
||||
if (sids === null || sids.filter(sid => initState.sources[sid].serviceRef !== undefined).length > 0)
|
||||
if (
|
||||
sids === null ||
|
||||
sids.filter(
|
||||
sid => initState.sources[sid].serviceRef !== undefined
|
||||
).length > 0
|
||||
)
|
||||
await dispatch(syncWithService(background))
|
||||
let timenow = new Date().getTime()
|
||||
const sourcesState = getState().sources
|
||||
let sources = (sids === null)
|
||||
? Object.values(sourcesState).filter(s => {
|
||||
let last = s.lastFetched ? s.lastFetched.getTime() : 0
|
||||
return !s.serviceRef && ((last > timenow) || (last + (s.fetchFrequency || 0) * 60000 <= timenow))
|
||||
})
|
||||
: sids.map(sid => sourcesState[sid]).filter(s => !s.serviceRef)
|
||||
let sources =
|
||||
sids === null
|
||||
? Object.values(sourcesState).filter(s => {
|
||||
let last = s.lastFetched ? s.lastFetched.getTime() : 0
|
||||
return (
|
||||
!s.serviceRef &&
|
||||
(last > timenow ||
|
||||
last + (s.fetchFrequency || 0) * 60000 <=
|
||||
timenow)
|
||||
)
|
||||
})
|
||||
: sids
|
||||
.map(sid => sourcesState[sid])
|
||||
.filter(s => !s.serviceRef)
|
||||
for (let source of sources) {
|
||||
let promise = RSSSource.fetchItems(source)
|
||||
promise.then(() => dispatch(updateSource({ ...source, lastFetched: new Date() })))
|
||||
promise.then(() =>
|
||||
dispatch(
|
||||
updateSource({ ...source, lastFetched: new Date() })
|
||||
)
|
||||
)
|
||||
promise.finally(() => dispatch(fetchItemsIntermediate()))
|
||||
promises.push(promise)
|
||||
}
|
||||
|
@ -201,48 +265,60 @@ export function fetchItems(background = false, sids: number[] = null): AppThunk<
|
|||
}
|
||||
})
|
||||
insertItems(items)
|
||||
.then(inserted => {
|
||||
dispatch(fetchItemsSuccess(inserted.reverse(), getState().items))
|
||||
resolve()
|
||||
if (background) {
|
||||
for (let item of inserted) {
|
||||
if (item.notify) {
|
||||
dispatch(pushNotification(item))
|
||||
.then(inserted => {
|
||||
dispatch(
|
||||
fetchItemsSuccess(
|
||||
inserted.reverse(),
|
||||
getState().items
|
||||
)
|
||||
)
|
||||
resolve()
|
||||
if (background) {
|
||||
for (let item of inserted) {
|
||||
if (item.notify) {
|
||||
dispatch(pushNotification(item))
|
||||
}
|
||||
}
|
||||
if (inserted.length > 0) {
|
||||
window.utils.requestAttention()
|
||||
}
|
||||
} else {
|
||||
dispatch(dismissItems())
|
||||
}
|
||||
if (inserted.length > 0) {
|
||||
window.utils.requestAttention()
|
||||
}
|
||||
} else {
|
||||
dispatch(dismissItems())
|
||||
}
|
||||
dispatch(setupAutoFetch())
|
||||
})
|
||||
.catch(err => {
|
||||
dispatch(fetchItemsSuccess([], getState().items))
|
||||
window.utils.showErrorBox("A database error has occurred.", String(err))
|
||||
console.log(err)
|
||||
reject(err)
|
||||
})
|
||||
dispatch(setupAutoFetch())
|
||||
})
|
||||
.catch(err => {
|
||||
dispatch(fetchItemsSuccess([], getState().items))
|
||||
window.utils.showErrorBox(
|
||||
"A database error has occurred.",
|
||||
String(err)
|
||||
)
|
||||
console.log(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const markReadDone = (item: RSSItem): ItemActionTypes => ({
|
||||
type: MARK_READ,
|
||||
item: item
|
||||
const markReadDone = (item: RSSItem): ItemActionTypes => ({
|
||||
type: MARK_READ,
|
||||
item: item,
|
||||
})
|
||||
|
||||
const markUnreadDone = (item: RSSItem): ItemActionTypes => ({
|
||||
type: MARK_UNREAD,
|
||||
item: item
|
||||
const markUnreadDone = (item: RSSItem): ItemActionTypes => ({
|
||||
type: MARK_UNREAD,
|
||||
item: item,
|
||||
})
|
||||
|
||||
export function markRead(item: RSSItem): AppThunk {
|
||||
return (dispatch) => {
|
||||
return dispatch => {
|
||||
if (!item.hasRead) {
|
||||
db.itemsDB.update(db.items).where(db.items._id.eq(item._id)).set(db.items.hasRead, true).exec()
|
||||
db.itemsDB
|
||||
.update(db.items)
|
||||
.where(db.items._id.eq(item._id))
|
||||
.set(db.items.hasRead, true)
|
||||
.exec()
|
||||
dispatch(markReadDone(item))
|
||||
if (item.serviceRef) {
|
||||
dispatch(dispatch(getServiceHooks()).markRead?.(item))
|
||||
|
@ -251,45 +327,63 @@ export function markRead(item: RSSItem): AppThunk {
|
|||
}
|
||||
}
|
||||
|
||||
export function markAllRead(sids: number[] = null, date: Date = null, before = true): AppThunk<Promise<void>> {
|
||||
export function markAllRead(
|
||||
sids: number[] = null,
|
||||
date: Date = null,
|
||||
before = true
|
||||
): AppThunk<Promise<void>> {
|
||||
return async (dispatch, getState) => {
|
||||
let state = getState()
|
||||
if (sids === null) {
|
||||
let feed = state.feeds[state.page.feedId]
|
||||
sids = feed.sids
|
||||
}
|
||||
const action = dispatch(getServiceHooks()).markAllRead?.(sids, date, before)
|
||||
const action = dispatch(getServiceHooks()).markAllRead?.(
|
||||
sids,
|
||||
date,
|
||||
before
|
||||
)
|
||||
if (action) await dispatch(action)
|
||||
const predicates: lf.Predicate[] = [
|
||||
db.items.source.in(sids),
|
||||
db.items.hasRead.eq(false)
|
||||
db.items.hasRead.eq(false),
|
||||
]
|
||||
if (date) {
|
||||
predicates.push(before ? db.items.date.lte(date) : db.items.date.gte(date))
|
||||
predicates.push(
|
||||
before ? db.items.date.lte(date) : db.items.date.gte(date)
|
||||
)
|
||||
}
|
||||
const query = lf.op.and.apply(null, predicates)
|
||||
await db.itemsDB.update(db.items).set(db.items.hasRead, true).where(query).exec()
|
||||
await db.itemsDB
|
||||
.update(db.items)
|
||||
.set(db.items.hasRead, true)
|
||||
.where(query)
|
||||
.exec()
|
||||
if (date) {
|
||||
dispatch({
|
||||
type: MARK_ALL_READ,
|
||||
sids: sids,
|
||||
time: date.getTime(),
|
||||
before: before
|
||||
before: before,
|
||||
})
|
||||
dispatch(updateUnreadCounts())
|
||||
} else {
|
||||
dispatch({
|
||||
type: MARK_ALL_READ,
|
||||
sids: sids
|
||||
sids: sids,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function markUnread(item: RSSItem): AppThunk {
|
||||
return (dispatch) => {
|
||||
return dispatch => {
|
||||
if (item.hasRead) {
|
||||
db.itemsDB.update(db.items).where(db.items._id.eq(item._id)).set(db.items.hasRead, false).exec()
|
||||
db.itemsDB
|
||||
.update(db.items)
|
||||
.where(db.items._id.eq(item._id))
|
||||
.set(db.items.hasRead, false)
|
||||
.exec()
|
||||
dispatch(markUnreadDone(item))
|
||||
if (item.serviceRef) {
|
||||
dispatch(dispatch(getServiceHooks()).markUnread?.(item))
|
||||
|
@ -298,14 +392,18 @@ export function markUnread(item: RSSItem): AppThunk {
|
|||
}
|
||||
}
|
||||
|
||||
const toggleStarredDone = (item: RSSItem): ItemActionTypes => ({
|
||||
type: TOGGLE_STARRED,
|
||||
item: item
|
||||
const toggleStarredDone = (item: RSSItem): ItemActionTypes => ({
|
||||
type: TOGGLE_STARRED,
|
||||
item: item,
|
||||
})
|
||||
|
||||
export function toggleStarred(item: RSSItem): AppThunk {
|
||||
return (dispatch) => {
|
||||
db.itemsDB.update(db.items).where(db.items._id.eq(item._id)).set(db.items.starred, !item.starred).exec()
|
||||
return dispatch => {
|
||||
db.itemsDB
|
||||
.update(db.items)
|
||||
.where(db.items._id.eq(item._id))
|
||||
.set(db.items.starred, !item.starred)
|
||||
.exec()
|
||||
dispatch(toggleStarredDone(item))
|
||||
if (item.serviceRef) {
|
||||
const hooks = dispatch(getServiceHooks())
|
||||
|
@ -315,34 +413,42 @@ export function toggleStarred(item: RSSItem): AppThunk {
|
|||
}
|
||||
}
|
||||
|
||||
const toggleHiddenDone = (item: RSSItem): ItemActionTypes => ({
|
||||
type: TOGGLE_HIDDEN,
|
||||
item: item
|
||||
const toggleHiddenDone = (item: RSSItem): ItemActionTypes => ({
|
||||
type: TOGGLE_HIDDEN,
|
||||
item: item,
|
||||
})
|
||||
|
||||
export function toggleHidden(item: RSSItem): AppThunk {
|
||||
return (dispatch) => {
|
||||
db.itemsDB.update(db.items).where(db.items._id.eq(item._id)).set(db.items.hidden, !item.hidden).exec()
|
||||
return dispatch => {
|
||||
db.itemsDB
|
||||
.update(db.items)
|
||||
.where(db.items._id.eq(item._id))
|
||||
.set(db.items.hidden, !item.hidden)
|
||||
.exec()
|
||||
dispatch(toggleHiddenDone(item))
|
||||
}
|
||||
}
|
||||
|
||||
export function itemShortcuts(item: RSSItem, e: KeyboardEvent): AppThunk {
|
||||
return (dispatch) => {
|
||||
return dispatch => {
|
||||
if (e.metaKey) return
|
||||
switch (e.key) {
|
||||
case "m": case "M":
|
||||
case "m":
|
||||
case "M":
|
||||
if (item.hasRead) dispatch(markUnread(item))
|
||||
else dispatch(markRead(item))
|
||||
break
|
||||
case "b": case "B":
|
||||
case "b":
|
||||
case "B":
|
||||
if (!item.hasRead) dispatch(markRead(item))
|
||||
window.utils.openExternal(item.link, platformCtrl(e))
|
||||
break
|
||||
case "s": case "S":
|
||||
case "s":
|
||||
case "S":
|
||||
dispatch(toggleStarred(item))
|
||||
break
|
||||
case "h": case "H":
|
||||
case "h":
|
||||
case "H":
|
||||
if (!item.hasRead && !item.hidden) dispatch(markRead(item))
|
||||
dispatch(toggleHidden(item))
|
||||
break
|
||||
|
@ -372,7 +478,11 @@ export function applyItemReduction(item: RSSItem, type: string) {
|
|||
|
||||
export function itemReducer(
|
||||
state: ItemState = {},
|
||||
action: ItemActionTypes | FeedActionTypes | ServiceActionTypes | SettingsActionTypes
|
||||
action:
|
||||
| ItemActionTypes
|
||||
| FeedActionTypes
|
||||
| ServiceActionTypes
|
||||
| SettingsActionTypes
|
||||
): ItemState {
|
||||
switch (action.type) {
|
||||
case FETCH_ITEMS:
|
||||
|
@ -382,9 +492,10 @@ export function itemReducer(
|
|||
for (let i of action.items) {
|
||||
newMap[i._id] = i
|
||||
}
|
||||
return {...newMap, ...state}
|
||||
return { ...newMap, ...state }
|
||||
}
|
||||
default: return state
|
||||
default:
|
||||
return state
|
||||
}
|
||||
case MARK_UNREAD:
|
||||
case MARK_READ:
|
||||
|
@ -392,7 +503,10 @@ export function itemReducer(
|
|||
case TOGGLE_HIDDEN: {
|
||||
return {
|
||||
...state,
|
||||
[action.item._id]: applyItemReduction(state[action.item._id], action.type)
|
||||
[action.item._id]: applyItemReduction(
|
||||
state[action.item._id],
|
||||
action.type
|
||||
),
|
||||
}
|
||||
}
|
||||
case MARK_ALL_READ: {
|
||||
|
@ -400,13 +514,15 @@ export function itemReducer(
|
|||
let sids = new Set(action.sids)
|
||||
for (let item of Object.values(state)) {
|
||||
if (sids.has(item.source) && !item.hasRead) {
|
||||
if (!action.time || (action.before
|
||||
? item.date.getTime() <= action.time
|
||||
: item.date.getTime() >= action.time)
|
||||
if (
|
||||
!action.time ||
|
||||
(action.before
|
||||
? item.date.getTime() <= action.time
|
||||
: item.date.getTime() >= action.time)
|
||||
) {
|
||||
nextState[item._id] = {
|
||||
...item,
|
||||
hasRead: true
|
||||
hasRead: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -423,7 +539,8 @@ export function itemReducer(
|
|||
}
|
||||
return nextState
|
||||
}
|
||||
default: return state
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
case SYNC_LOCAL_ITEMS: {
|
||||
|
@ -445,6 +562,7 @@ export function itemReducer(
|
|||
}
|
||||
return nextState
|
||||
}
|
||||
default: return state
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,13 @@
|
|||
import { ALL, SOURCE, loadMore, FeedFilter, FilterType, initFeeds, FeedActionTypes, INIT_FEED } from "./feed"
|
||||
import {
|
||||
ALL,
|
||||
SOURCE,
|
||||
loadMore,
|
||||
FeedFilter,
|
||||
FilterType,
|
||||
initFeeds,
|
||||
FeedActionTypes,
|
||||
INIT_FEED,
|
||||
} from "./feed"
|
||||
import { getWindowBreakpoint, AppThunk, ActionStatus } from "../utils"
|
||||
import { RSSItem, markRead } from "./item"
|
||||
import { SourceActionTypes, DELETE_SOURCE } from "./source"
|
||||
|
@ -15,7 +24,9 @@ export const APPLY_FILTER = "APPLY_FILTER"
|
|||
export const TOGGLE_SEARCH = "TOGGLE_SEARCH"
|
||||
|
||||
export enum PageType {
|
||||
AllArticles, Sources, Page
|
||||
AllArticles,
|
||||
Sources,
|
||||
Page,
|
||||
}
|
||||
|
||||
interface SelectPageAction {
|
||||
|
@ -50,11 +61,21 @@ interface ApplyFilterAction {
|
|||
filter: FeedFilter
|
||||
}
|
||||
|
||||
interface DismissItemAction { type: typeof DISMISS_ITEM }
|
||||
interface ToggleSearchAction { type: typeof TOGGLE_SEARCH }
|
||||
interface DismissItemAction {
|
||||
type: typeof DISMISS_ITEM
|
||||
}
|
||||
interface ToggleSearchAction {
|
||||
type: typeof TOGGLE_SEARCH
|
||||
}
|
||||
|
||||
export type PageActionTypes = SelectPageAction | SwitchViewAction | ShowItemAction
|
||||
| DismissItemAction | ApplyFilterAction | ToggleSearchAction | SetViewConfigsAction
|
||||
export type PageActionTypes =
|
||||
| SelectPageAction
|
||||
| SwitchViewAction
|
||||
| ShowItemAction
|
||||
| DismissItemAction
|
||||
| ApplyFilterAction
|
||||
| ToggleSearchAction
|
||||
| SetViewConfigsAction
|
||||
|
||||
export function selectAllArticles(init = false): AppThunk {
|
||||
return (dispatch, getState) => {
|
||||
|
@ -63,12 +84,16 @@ export function selectAllArticles(init = false): AppThunk {
|
|||
keepMenu: getWindowBreakpoint(),
|
||||
filter: getState().page.filter,
|
||||
pageType: PageType.AllArticles,
|
||||
init: init
|
||||
init: init,
|
||||
} as PageActionTypes)
|
||||
}
|
||||
}
|
||||
|
||||
export function selectSources(sids: number[], menuKey: string, title: string): AppThunk {
|
||||
export function selectSources(
|
||||
sids: number[],
|
||||
menuKey: string,
|
||||
title: string
|
||||
): AppThunk {
|
||||
return (dispatch, getState) => {
|
||||
if (getState().app.menuKey !== menuKey) {
|
||||
dispatch({
|
||||
|
@ -79,16 +104,16 @@ export function selectSources(sids: number[], menuKey: string, title: string): A
|
|||
sids: sids,
|
||||
menuKey: menuKey,
|
||||
title: title,
|
||||
init: true
|
||||
init: true,
|
||||
} as PageActionTypes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function switchView(viewType: ViewType): PageActionTypes {
|
||||
return {
|
||||
type: SWITCH_VIEW,
|
||||
viewType: viewType
|
||||
viewType: viewType,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -97,7 +122,7 @@ export function setViewConfigs(configs: ViewConfigs): AppThunk {
|
|||
window.settings.setViewConfigs(getState().page.viewType, configs)
|
||||
dispatch({
|
||||
type: "SET_VIEW_CONFIGS",
|
||||
configs: configs
|
||||
configs: configs,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -105,11 +130,14 @@ export function setViewConfigs(configs: ViewConfigs): AppThunk {
|
|||
export function showItem(feedId: string, item: RSSItem): AppThunk {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState()
|
||||
if (state.items.hasOwnProperty(item._id) && state.sources.hasOwnProperty(item.source)) {
|
||||
if (
|
||||
state.items.hasOwnProperty(item._id) &&
|
||||
state.sources.hasOwnProperty(item.source)
|
||||
) {
|
||||
dispatch({
|
||||
type: SHOW_ITEM,
|
||||
feedId: feedId,
|
||||
item: item
|
||||
item: item,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -128,15 +156,17 @@ export const dismissItem = (): PageActionTypes => ({ type: DISMISS_ITEM })
|
|||
export const toggleSearch = (): AppThunk => {
|
||||
return (dispatch, getState) => {
|
||||
let state = getState()
|
||||
dispatch(({ type: TOGGLE_SEARCH }))
|
||||
dispatch({ type: TOGGLE_SEARCH })
|
||||
if (!getWindowBreakpoint() && state.app.menu) {
|
||||
dispatch(toggleMenu())
|
||||
}
|
||||
if (state.page.searchOn) {
|
||||
dispatch(applyFilter({
|
||||
...state.page.filter,
|
||||
search: ""
|
||||
}))
|
||||
dispatch(
|
||||
applyFilter({
|
||||
...state.page.filter,
|
||||
search: "",
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -153,7 +183,9 @@ export function showOffsetItem(offset: number): AppThunk {
|
|||
if (itemIndex < 0) {
|
||||
let item = state.items[itemId]
|
||||
let prevs = feed.iids
|
||||
.map((id, index) => [state.items[id], index] as [RSSItem, number])
|
||||
.map(
|
||||
(id, index) => [state.items[id], index] as [RSSItem, number]
|
||||
)
|
||||
.filter(([i, _]) => i.date > item.date)
|
||||
if (prevs.length > 0) {
|
||||
let prev = prevs[0]
|
||||
|
@ -171,12 +203,12 @@ export function showOffsetItem(offset: number): AppThunk {
|
|||
dispatch(markRead(item))
|
||||
dispatch(showItem(feedId, item))
|
||||
return
|
||||
} else if (!feed.allLoaded){
|
||||
dispatch(loadMore(feed)).then(() => {
|
||||
dispatch(showOffsetItem(offset))
|
||||
}).catch(() =>
|
||||
dispatch(dismissItem())
|
||||
)
|
||||
} else if (!feed.allLoaded) {
|
||||
dispatch(loadMore(feed))
|
||||
.then(() => {
|
||||
dispatch(showOffsetItem(offset))
|
||||
})
|
||||
.catch(() => dispatch(dismissItem()))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -186,13 +218,14 @@ export function showOffsetItem(offset: number): AppThunk {
|
|||
|
||||
const applyFilterDone = (filter: FeedFilter): PageActionTypes => ({
|
||||
type: APPLY_FILTER,
|
||||
filter: filter
|
||||
filter: filter,
|
||||
})
|
||||
|
||||
function applyFilter(filter: FeedFilter): AppThunk {
|
||||
return (dispatch, getState) => {
|
||||
const oldFilterType = getState().page.filter.type
|
||||
if (filter.type !== oldFilterType) window.settings.setFilterType(filter.type)
|
||||
if (filter.type !== oldFilterType)
|
||||
window.settings.setFilterType(filter.type)
|
||||
dispatch(applyFilterDone(filter))
|
||||
dispatch(initFeeds(true))
|
||||
}
|
||||
|
@ -204,10 +237,12 @@ export function switchFilter(filter: FilterType): AppThunk {
|
|||
let oldType = oldFilter.type
|
||||
let newType = filter | (oldType & FilterType.Toggles)
|
||||
if (oldType != newType) {
|
||||
dispatch(applyFilter({
|
||||
...oldFilter,
|
||||
type: newType
|
||||
}))
|
||||
dispatch(
|
||||
applyFilter({
|
||||
...oldFilter,
|
||||
type: newType,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -224,17 +259,21 @@ export function performSearch(query: string): AppThunk {
|
|||
return (dispatch, getState) => {
|
||||
let state = getState()
|
||||
if (state.page.searchOn) {
|
||||
dispatch(applyFilter({
|
||||
...state.page.filter,
|
||||
search: query
|
||||
}))
|
||||
dispatch(
|
||||
applyFilter({
|
||||
...state.page.filter,
|
||||
search: query,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class PageState {
|
||||
viewType = window.settings.getDefaultView()
|
||||
viewConfigs = window.settings.getViewConfigs(window.settings.getDefaultView())
|
||||
viewConfigs = window.settings.getViewConfigs(
|
||||
window.settings.getDefaultView()
|
||||
)
|
||||
filter = new FeedFilter()
|
||||
feedId = ALL
|
||||
itemId = null as number
|
||||
|
@ -249,54 +288,71 @@ export function pageReducer(
|
|||
switch (action.type) {
|
||||
case SELECT_PAGE:
|
||||
switch (action.pageType) {
|
||||
case PageType.AllArticles: return {
|
||||
...state,
|
||||
feedId: ALL,
|
||||
itemId: null
|
||||
}
|
||||
case PageType.Sources: return {
|
||||
...state,
|
||||
feedId: SOURCE,
|
||||
itemId: null
|
||||
}
|
||||
default: return state
|
||||
case PageType.AllArticles:
|
||||
return {
|
||||
...state,
|
||||
feedId: ALL,
|
||||
itemId: null,
|
||||
}
|
||||
case PageType.Sources:
|
||||
return {
|
||||
...state,
|
||||
feedId: SOURCE,
|
||||
itemId: null,
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
case SWITCH_VIEW: return {
|
||||
...state,
|
||||
viewType: action.viewType,
|
||||
viewConfigs: window.settings.getViewConfigs(action.viewType),
|
||||
itemId: null
|
||||
}
|
||||
case SET_VIEW_CONFIGS: return {
|
||||
...state,
|
||||
viewConfigs: action.configs
|
||||
}
|
||||
case APPLY_FILTER: return {
|
||||
...state,
|
||||
filter: action.filter
|
||||
}
|
||||
case SHOW_ITEM: return {
|
||||
...state,
|
||||
itemId: action.item._id,
|
||||
itemFromFeed: Boolean(action.feedId)
|
||||
}
|
||||
case INIT_FEED: switch (action.status) {
|
||||
case ActionStatus.Success: return {
|
||||
case SWITCH_VIEW:
|
||||
return {
|
||||
...state,
|
||||
itemId: (action.feed._id === state.feedId && action.items.filter(i => i._id === state.itemId).length === 0)
|
||||
? null : state.itemId
|
||||
viewType: action.viewType,
|
||||
viewConfigs: window.settings.getViewConfigs(action.viewType),
|
||||
itemId: null,
|
||||
}
|
||||
case SET_VIEW_CONFIGS:
|
||||
return {
|
||||
...state,
|
||||
viewConfigs: action.configs,
|
||||
}
|
||||
case APPLY_FILTER:
|
||||
return {
|
||||
...state,
|
||||
filter: action.filter,
|
||||
}
|
||||
case SHOW_ITEM:
|
||||
return {
|
||||
...state,
|
||||
itemId: action.item._id,
|
||||
itemFromFeed: Boolean(action.feedId),
|
||||
}
|
||||
case INIT_FEED:
|
||||
switch (action.status) {
|
||||
case ActionStatus.Success:
|
||||
return {
|
||||
...state,
|
||||
itemId:
|
||||
action.feed._id === state.feedId &&
|
||||
action.items.filter(i => i._id === state.itemId)
|
||||
.length === 0
|
||||
? null
|
||||
: state.itemId,
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
default: return state
|
||||
}
|
||||
case DELETE_SOURCE:
|
||||
case DISMISS_ITEM: return {
|
||||
...state,
|
||||
itemId: null
|
||||
}
|
||||
case TOGGLE_SEARCH: return {
|
||||
...state,
|
||||
searchOn: !state.searchOn
|
||||
}
|
||||
default: return state
|
||||
case DISMISS_ITEM:
|
||||
return {
|
||||
...state,
|
||||
itemId: null,
|
||||
}
|
||||
case TOGGLE_SEARCH:
|
||||
return {
|
||||
...state,
|
||||
searchOn: !state.searchOn,
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@ import { FeedFilter, FilterType } from "./feed"
|
|||
import { RSSItem } from "./item"
|
||||
|
||||
export const enum ItemAction {
|
||||
Read = "r",
|
||||
Star = "s",
|
||||
Read = "r",
|
||||
Star = "s",
|
||||
Hide = "h",
|
||||
Notify = "n",
|
||||
}
|
||||
|
@ -49,7 +49,12 @@ export class SourceRule {
|
|||
match: boolean
|
||||
actions: RuleActions
|
||||
|
||||
constructor(regex: string, actions: string[], filter: FilterType, match: boolean) {
|
||||
constructor(
|
||||
regex: string,
|
||||
actions: string[],
|
||||
filter: FilterType,
|
||||
match: boolean
|
||||
) {
|
||||
this.filter = new FeedFilter(filter, regex)
|
||||
this.match = match
|
||||
this.actions = RuleActions.fromKeys(actions)
|
||||
|
@ -69,4 +74,4 @@ export class SourceRule {
|
|||
this.apply(rule, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,8 +4,15 @@ import { SyncService, ServiceConfigs } from "../../schema-types"
|
|||
import { AppThunk, ActionStatus } from "../utils"
|
||||
import { RSSItem, insertItems, fetchItemsSuccess } from "./item"
|
||||
import { saveSettings, pushNotification } from "./app"
|
||||
import { deleteSource, updateUnreadCounts, RSSSource, insertSource, addSourceSuccess,
|
||||
updateSource, updateFavicon } from "./source"
|
||||
import {
|
||||
deleteSource,
|
||||
updateUnreadCounts,
|
||||
RSSSource,
|
||||
insertSource,
|
||||
addSourceSuccess,
|
||||
updateSource,
|
||||
updateFavicon,
|
||||
} from "./source"
|
||||
import { createSourceGroup, addSourceToGroup } from "./group"
|
||||
|
||||
import { feverServiceHooks } from "./services/fever"
|
||||
|
@ -20,19 +27,26 @@ export interface ServiceHooks {
|
|||
syncItems?: () => AppThunk<Promise<[Set<string>, Set<string>]>>
|
||||
markRead?: (item: RSSItem) => AppThunk
|
||||
markUnread?: (item: RSSItem) => AppThunk
|
||||
markAllRead?: (sids?: number[], date?: Date, before?: boolean) => AppThunk<Promise<void>>
|
||||
markAllRead?: (
|
||||
sids?: number[],
|
||||
date?: Date,
|
||||
before?: boolean
|
||||
) => AppThunk<Promise<void>>
|
||||
star?: (item: RSSItem) => AppThunk
|
||||
unstar?: (item: RSSItem) => AppThunk
|
||||
}
|
||||
|
||||
export function getServiceHooksFromType(type: SyncService): ServiceHooks {
|
||||
switch (type) {
|
||||
case SyncService.Fever: return feverServiceHooks
|
||||
case SyncService.Feedbin: return feedbinServiceHooks
|
||||
case SyncService.Fever:
|
||||
return feverServiceHooks
|
||||
case SyncService.Feedbin:
|
||||
return feedbinServiceHooks
|
||||
case SyncService.GReader:
|
||||
case SyncService.Inoreader:
|
||||
return gReaderServiceHooks
|
||||
default: return {}
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,7 +63,7 @@ export function syncWithService(background = false): AppThunk<Promise<void>> {
|
|||
try {
|
||||
dispatch({
|
||||
type: SYNC_SERVICE,
|
||||
status: ActionStatus.Request
|
||||
status: ActionStatus.Request,
|
||||
})
|
||||
if (hooks.reauthenticate) await dispatch(reauthenticate(hooks))
|
||||
await dispatch(updateSources(hooks.updateSources))
|
||||
|
@ -57,14 +71,14 @@ export function syncWithService(background = false): AppThunk<Promise<void>> {
|
|||
await dispatch(fetchItems(hooks.fetchItems, background))
|
||||
dispatch({
|
||||
type: SYNC_SERVICE,
|
||||
status: ActionStatus.Success
|
||||
status: ActionStatus.Success,
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
dispatch({
|
||||
type: SYNC_SERVICE,
|
||||
status: ActionStatus.Failure,
|
||||
err: err
|
||||
err: err,
|
||||
})
|
||||
} finally {
|
||||
if (getState().app.settings.saving) dispatch(saveSettings())
|
||||
|
@ -83,8 +97,10 @@ function reauthenticate(hooks: ServiceHooks): AppThunk<Promise<void>> {
|
|||
}
|
||||
}
|
||||
|
||||
function updateSources(hook: ServiceHooks["updateSources"]): AppThunk<Promise<void>> {
|
||||
return async (dispatch, getState) => {
|
||||
function updateSources(
|
||||
hook: ServiceHooks["updateSources"]
|
||||
): AppThunk<Promise<void>> {
|
||||
return async (dispatch, getState) => {
|
||||
const [sources, groupsMap] = await dispatch(hook())
|
||||
const existing = new Map<string, RSSSource>()
|
||||
for (let source of Object.values(getState().sources)) {
|
||||
|
@ -93,17 +109,19 @@ function updateSources(hook: ServiceHooks["updateSources"]): AppThunk<Promise<vo
|
|||
}
|
||||
}
|
||||
const forceSettings = () => {
|
||||
if (!(getState().app.settings.saving)) dispatch(saveSettings())
|
||||
if (!getState().app.settings.saving) dispatch(saveSettings())
|
||||
}
|
||||
let promises = sources.map(async (s) => {
|
||||
let promises = sources.map(async s => {
|
||||
if (existing.has(s.serviceRef)) {
|
||||
const doc = existing.get(s.serviceRef)
|
||||
existing.delete(s.serviceRef)
|
||||
return doc
|
||||
} else {
|
||||
const docs = (await db.sourcesDB.select().from(db.sources).where(
|
||||
db.sources.url.eq(s.url)
|
||||
).exec()) as RSSSource[]
|
||||
const docs = (await db.sourcesDB
|
||||
.select()
|
||||
.from(db.sources)
|
||||
.where(db.sources.url.eq(s.url))
|
||||
.exec()) as RSSSource[]
|
||||
if (docs.length === 0) {
|
||||
// Create a new source
|
||||
forceSettings()
|
||||
|
@ -120,7 +138,11 @@ function updateSources(hook: ServiceHooks["updateSources"]): AppThunk<Promise<vo
|
|||
doc.serviceRef = s.serviceRef
|
||||
doc.unreadCount = 0
|
||||
await dispatch(updateSource(doc))
|
||||
await db.itemsDB.delete().from(db.items).where(db.items.source.eq(doc.sid)).exec()
|
||||
await db.itemsDB
|
||||
.delete()
|
||||
.from(db.items)
|
||||
.where(db.items.source.eq(doc.sid))
|
||||
.exec()
|
||||
return doc
|
||||
} else {
|
||||
return docs[0]
|
||||
|
@ -138,7 +160,9 @@ function updateSources(hook: ServiceHooks["updateSources"]): AppThunk<Promise<vo
|
|||
forceSettings()
|
||||
for (let source of sourcesResults) {
|
||||
if (groupsMap.has(source.serviceRef)) {
|
||||
const gid = dispatch(createSourceGroup(groupsMap.get(source.serviceRef)))
|
||||
const gid = dispatch(
|
||||
createSourceGroup(groupsMap.get(source.serviceRef))
|
||||
)
|
||||
dispatch(addSourceToGroup(gid, source.sid))
|
||||
}
|
||||
}
|
||||
|
@ -150,32 +174,59 @@ function updateSources(hook: ServiceHooks["updateSources"]): AppThunk<Promise<vo
|
|||
}
|
||||
|
||||
function syncItems(hook: ServiceHooks["syncItems"]): AppThunk<Promise<void>> {
|
||||
return async (dispatch, getState) => {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState()
|
||||
const [unreadRefs, starredRefs] = await dispatch(hook())
|
||||
const unreadCopy = new Set(unreadRefs)
|
||||
const starredCopy = new Set(starredRefs)
|
||||
const rows = await db.itemsDB.select(
|
||||
db.items.serviceRef, db.items.hasRead, db.items.starred
|
||||
).from(db.items).where(lf.op.and(
|
||||
db.items.serviceRef.isNotNull(),
|
||||
lf.op.or(db.items.hasRead.eq(false), db.items.starred.eq(true))
|
||||
)).exec()
|
||||
const rows = await db.itemsDB
|
||||
.select(db.items.serviceRef, db.items.hasRead, db.items.starred)
|
||||
.from(db.items)
|
||||
.where(
|
||||
lf.op.and(
|
||||
db.items.serviceRef.isNotNull(),
|
||||
lf.op.or(
|
||||
db.items.hasRead.eq(false),
|
||||
db.items.starred.eq(true)
|
||||
)
|
||||
)
|
||||
)
|
||||
.exec()
|
||||
const updates = new Array<lf.query.Update>()
|
||||
for (let row of rows) {
|
||||
const serviceRef = row["serviceRef"]
|
||||
if (row["hasRead"] === false && !unreadRefs.delete(serviceRef)) {
|
||||
updates.push(db.itemsDB.update(db.items).set(db.items.hasRead, true).where(db.items.serviceRef.eq(serviceRef)))
|
||||
updates.push(
|
||||
db.itemsDB
|
||||
.update(db.items)
|
||||
.set(db.items.hasRead, true)
|
||||
.where(db.items.serviceRef.eq(serviceRef))
|
||||
)
|
||||
}
|
||||
if (row["starred"] === true && !starredRefs.delete(serviceRef)) {
|
||||
updates.push(db.itemsDB.update(db.items).set(db.items.starred, false).where(db.items.serviceRef.eq(serviceRef)))
|
||||
updates.push(
|
||||
db.itemsDB
|
||||
.update(db.items)
|
||||
.set(db.items.starred, false)
|
||||
.where(db.items.serviceRef.eq(serviceRef))
|
||||
)
|
||||
}
|
||||
}
|
||||
for (let unread of unreadRefs) {
|
||||
updates.push(db.itemsDB.update(db.items).set(db.items.hasRead, false).where(db.items.serviceRef.eq(unread)))
|
||||
updates.push(
|
||||
db.itemsDB
|
||||
.update(db.items)
|
||||
.set(db.items.hasRead, false)
|
||||
.where(db.items.serviceRef.eq(unread))
|
||||
)
|
||||
}
|
||||
for (let starred of starredRefs) {
|
||||
updates.push(db.itemsDB.update(db.items).set(db.items.starred, true).where(db.items.serviceRef.eq(starred)))
|
||||
updates.push(
|
||||
db.itemsDB
|
||||
.update(db.items)
|
||||
.set(db.items.starred, true)
|
||||
.where(db.items.serviceRef.eq(starred))
|
||||
)
|
||||
}
|
||||
if (updates.length > 0) {
|
||||
await db.itemsDB.createTransaction().exec(updates)
|
||||
|
@ -185,7 +236,10 @@ function syncItems(hook: ServiceHooks["syncItems"]): AppThunk<Promise<void>> {
|
|||
}
|
||||
}
|
||||
|
||||
function fetchItems(hook: ServiceHooks["fetchItems"], background: boolean): AppThunk<Promise<void>> {
|
||||
function fetchItems(
|
||||
hook: ServiceHooks["fetchItems"],
|
||||
background: boolean
|
||||
): AppThunk<Promise<void>> {
|
||||
return async (dispatch, getState) => {
|
||||
const [items, configs] = await dispatch(hook())
|
||||
if (items.length > 0) {
|
||||
|
@ -218,9 +272,11 @@ export function removeService(): AppThunk<Promise<void>> {
|
|||
return async (dispatch, getState) => {
|
||||
dispatch(saveSettings())
|
||||
const state = getState()
|
||||
const promises = Object.values(state.sources).filter(s => s.serviceRef).map(async s => {
|
||||
await dispatch(deleteSource(s, true))
|
||||
})
|
||||
const promises = Object.values(state.sources)
|
||||
.filter(s => s.serviceRef)
|
||||
.map(async s => {
|
||||
await dispatch(deleteSource(s, true))
|
||||
})
|
||||
await Promise.all(promises)
|
||||
dispatch(saveServiceConfigs({ type: SyncService.None }))
|
||||
dispatch(saveSettings())
|
||||
|
@ -248,23 +304,29 @@ interface SyncLocalItemsAction {
|
|||
starredIds: Set<string>
|
||||
}
|
||||
|
||||
export type ServiceActionTypes = SaveServiceConfigsAction | SyncWithServiceAction | SyncLocalItemsAction
|
||||
export type ServiceActionTypes =
|
||||
| SaveServiceConfigsAction
|
||||
| SyncWithServiceAction
|
||||
| SyncLocalItemsAction
|
||||
|
||||
export function saveServiceConfigs(configs: ServiceConfigs): AppThunk {
|
||||
return (dispatch) => {
|
||||
return dispatch => {
|
||||
window.settings.setServiceConfigs(configs)
|
||||
dispatch({
|
||||
type: SAVE_SERVICE_CONFIGS,
|
||||
configs: configs
|
||||
configs: configs,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function syncLocalItems(unread: Set<string>, starred: Set<string>): ServiceActionTypes {
|
||||
function syncLocalItems(
|
||||
unread: Set<string>,
|
||||
starred: Set<string>
|
||||
): ServiceActionTypes {
|
||||
return {
|
||||
type: SYNC_LOCAL_ITEMS,
|
||||
unreadIds: unread,
|
||||
starredIds: starred
|
||||
starredIds: starred,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -273,7 +335,9 @@ export function serviceReducer(
|
|||
action: ServiceActionTypes
|
||||
): ServiceConfigs {
|
||||
switch (action.type) {
|
||||
case SAVE_SERVICE_CONFIGS: return action.configs
|
||||
default: return state
|
||||
case SAVE_SERVICE_CONFIGS:
|
||||
return action.configs
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,13 +20,24 @@ export interface FeedbinConfigs extends ServiceConfigs {
|
|||
|
||||
async function fetchAPI(configs: FeedbinConfigs, params: string) {
|
||||
const headers = new Headers()
|
||||
headers.set("Authorization", "Basic " + btoa(configs.username + ":" + configs.password))
|
||||
headers.set(
|
||||
"Authorization",
|
||||
"Basic " + btoa(configs.username + ":" + configs.password)
|
||||
)
|
||||
return await fetch(configs.endpoint + params, { headers: headers })
|
||||
}
|
||||
|
||||
async function markItems(configs: FeedbinConfigs, type: string, method: string, refs: number[]) {
|
||||
async function markItems(
|
||||
configs: FeedbinConfigs,
|
||||
type: string,
|
||||
method: string,
|
||||
refs: number[]
|
||||
) {
|
||||
const headers = new Headers()
|
||||
headers.set("Authorization", "Basic " + btoa(configs.username + ":" + configs.password))
|
||||
headers.set(
|
||||
"Authorization",
|
||||
"Basic " + btoa(configs.username + ":" + configs.password)
|
||||
)
|
||||
headers.set("Content-Type", "application/json; charset=utf-8")
|
||||
const promises = new Array<Promise<Response>>()
|
||||
while (refs.length > 0) {
|
||||
|
@ -36,16 +47,18 @@ async function markItems(configs: FeedbinConfigs, type: string, method: string,
|
|||
}
|
||||
const bodyObject: any = {}
|
||||
bodyObject[`${type}_entries`] = batch
|
||||
promises.push(fetch(configs.endpoint + type + "_entries.json", {
|
||||
method: method,
|
||||
headers: headers,
|
||||
body: JSON.stringify(bodyObject)
|
||||
}))
|
||||
promises.push(
|
||||
fetch(configs.endpoint + type + "_entries.json", {
|
||||
method: method,
|
||||
headers: headers,
|
||||
body: JSON.stringify(bodyObject),
|
||||
})
|
||||
)
|
||||
}
|
||||
return await Promise.all(promises)
|
||||
}
|
||||
|
||||
const APIError = () => new Error(intl.get("service.failure"))
|
||||
const APIError = () => new Error(intl.get("service.failure"))
|
||||
|
||||
export const feedbinServiceHooks: ServiceHooks = {
|
||||
authenticate: async (configs: FeedbinConfigs) => {
|
||||
|
@ -89,13 +102,17 @@ export const feedbinServiceHooks: ServiceHooks = {
|
|||
syncItems: () => async (_, getState) => {
|
||||
const configs = getState().service as FeedbinConfigs
|
||||
const [unreadResponse, starredResponse] = await Promise.all([
|
||||
fetchAPI(configs, "unread_entries.json"),
|
||||
fetchAPI(configs, "starred_entries.json")
|
||||
fetchAPI(configs, "unread_entries.json"),
|
||||
fetchAPI(configs, "starred_entries.json"),
|
||||
])
|
||||
if (unreadResponse.status !== 200 || starredResponse.status !== 200) throw APIError()
|
||||
if (unreadResponse.status !== 200 || starredResponse.status !== 200)
|
||||
throw APIError()
|
||||
const unread = await unreadResponse.json()
|
||||
const starred = await starredResponse.json()
|
||||
return [new Set(unread.map(i => String(i))), new Set(starred.map(i => String(i)))]
|
||||
return [
|
||||
new Set(unread.map(i => String(i))),
|
||||
new Set(starred.map(i => String(i))),
|
||||
]
|
||||
},
|
||||
|
||||
fetchItems: () => async (_, getState) => {
|
||||
|
@ -108,10 +125,17 @@ export const feedbinServiceHooks: ServiceHooks = {
|
|||
let lastFetched: any[]
|
||||
do {
|
||||
try {
|
||||
const response = await fetchAPI(configs, "entries.json?mode=extended&per_page=125&page=" + page)
|
||||
const response = await fetchAPI(
|
||||
configs,
|
||||
"entries.json?mode=extended&per_page=125&page=" + page
|
||||
)
|
||||
if (response.status !== 200) throw APIError()
|
||||
lastFetched = await response.json()
|
||||
items.push(...lastFetched.filter(i => i.id > configs.lastId && i.id < min))
|
||||
items.push(
|
||||
...lastFetched.filter(
|
||||
i => i.id > configs.lastId && i.id < min
|
||||
)
|
||||
)
|
||||
min = lastFetched.reduce((m, n) => Math.min(m, n.id), min)
|
||||
page += 1
|
||||
} catch {
|
||||
|
@ -119,10 +143,14 @@ export const feedbinServiceHooks: ServiceHooks = {
|
|||
}
|
||||
} while (
|
||||
min > configs.lastId &&
|
||||
lastFetched && lastFetched.length >= 125 &&
|
||||
lastFetched &&
|
||||
lastFetched.length >= 125 &&
|
||||
items.length < configs.fetchLimit
|
||||
)
|
||||
configs.lastId = items.reduce((m, n) => Math.max(m, n.id), configs.lastId)
|
||||
configs.lastId = items.reduce(
|
||||
(m, n) => Math.max(m, n.id),
|
||||
configs.lastId
|
||||
)
|
||||
if (items.length > 0) {
|
||||
const fidMap = new Map<string, RSSSource>()
|
||||
for (let source of Object.values(state.sources)) {
|
||||
|
@ -131,10 +159,11 @@ export const feedbinServiceHooks: ServiceHooks = {
|
|||
}
|
||||
}
|
||||
const [unreadResponse, starredResponse] = await Promise.all([
|
||||
fetchAPI(configs, "unread_entries.json"),
|
||||
fetchAPI(configs, "starred_entries.json")
|
||||
fetchAPI(configs, "unread_entries.json"),
|
||||
fetchAPI(configs, "starred_entries.json"),
|
||||
])
|
||||
if (unreadResponse.status !== 200 || starredResponse.status !== 200) throw APIError()
|
||||
if (unreadResponse.status !== 200 || starredResponse.status !== 200)
|
||||
throw APIError()
|
||||
const unread: Set<number> = new Set(await unreadResponse.json())
|
||||
const starred: Set<number> = new Set(await starredResponse.json())
|
||||
const parsedItems = new Array<RSSItem>()
|
||||
|
@ -160,8 +189,11 @@ export const feedbinServiceHooks: ServiceHooks = {
|
|||
if (i.images && i.images.original_url) {
|
||||
item.thumb = i.images.original_url
|
||||
} else {
|
||||
let baseEl = dom.createElement('base')
|
||||
baseEl.setAttribute('href', item.link.split("/").slice(0, 3).join("/"))
|
||||
let baseEl = dom.createElement("base")
|
||||
baseEl.setAttribute(
|
||||
"href",
|
||||
item.link.split("/").slice(0, 3).join("/")
|
||||
)
|
||||
dom.head.append(baseEl)
|
||||
let img = dom.querySelector("img")
|
||||
if (img && img.src) item.thumb = img.src
|
||||
|
@ -169,9 +201,19 @@ export const feedbinServiceHooks: ServiceHooks = {
|
|||
// Apply rules and sync back to the service
|
||||
if (source.rules) SourceRule.applyAll(source.rules, item)
|
||||
if (unread.has(i.id) === item.hasRead)
|
||||
markItems(configs, "unread", item.hasRead ? "DELETE" : "POST", [i.id])
|
||||
markItems(
|
||||
configs,
|
||||
"unread",
|
||||
item.hasRead ? "DELETE" : "POST",
|
||||
[i.id]
|
||||
)
|
||||
if (starred.has(i.id) !== Boolean(item.starred))
|
||||
markItems(configs, "starred", item.starred ? "POST" : "DELETE", [i.id])
|
||||
markItems(
|
||||
configs,
|
||||
"starred",
|
||||
item.starred ? "POST" : "DELETE",
|
||||
[i.id]
|
||||
)
|
||||
parsedItems.push(item)
|
||||
})
|
||||
return [parsedItems, configs]
|
||||
|
@ -186,30 +228,56 @@ export const feedbinServiceHooks: ServiceHooks = {
|
|||
const predicates: lf.Predicate[] = [
|
||||
db.items.source.in(sids),
|
||||
db.items.hasRead.eq(false),
|
||||
db.items.serviceRef.isNotNull()
|
||||
db.items.serviceRef.isNotNull(),
|
||||
]
|
||||
if (date) {
|
||||
predicates.push(before ? db.items.date.lte(date) : db.items.date.gte(date))
|
||||
predicates.push(
|
||||
before ? db.items.date.lte(date) : db.items.date.gte(date)
|
||||
)
|
||||
}
|
||||
const query = lf.op.and.apply(null, predicates)
|
||||
const rows = await db.itemsDB.select(db.items.serviceRef).from(db.items).where(query).exec()
|
||||
const rows = await db.itemsDB
|
||||
.select(db.items.serviceRef)
|
||||
.from(db.items)
|
||||
.where(query)
|
||||
.exec()
|
||||
const refs = rows.map(row => parseInt(row["serviceRef"]))
|
||||
markItems(configs, "unread", "DELETE", refs)
|
||||
},
|
||||
|
||||
markRead: (item: RSSItem) => async (_, getState) => {
|
||||
await markItems(getState().service as FeedbinConfigs, "unread", "DELETE", [parseInt(item.serviceRef)])
|
||||
await markItems(
|
||||
getState().service as FeedbinConfigs,
|
||||
"unread",
|
||||
"DELETE",
|
||||
[parseInt(item.serviceRef)]
|
||||
)
|
||||
},
|
||||
|
||||
markUnread: (item: RSSItem) => async (_, getState) => {
|
||||
await markItems(getState().service as FeedbinConfigs, "unread", "POST", [parseInt(item.serviceRef)])
|
||||
await markItems(
|
||||
getState().service as FeedbinConfigs,
|
||||
"unread",
|
||||
"POST",
|
||||
[parseInt(item.serviceRef)]
|
||||
)
|
||||
},
|
||||
|
||||
star: (item: RSSItem) => async (_, getState) => {
|
||||
await markItems(getState().service as FeedbinConfigs, "starred", "POST", [parseInt(item.serviceRef)])
|
||||
await markItems(
|
||||
getState().service as FeedbinConfigs,
|
||||
"starred",
|
||||
"POST",
|
||||
[parseInt(item.serviceRef)]
|
||||
)
|
||||
},
|
||||
|
||||
unstar: (item: RSSItem) => async (_, getState) => {
|
||||
await markItems(getState().service as FeedbinConfigs, "starred", "DELETE", [parseInt(item.serviceRef)])
|
||||
await markItems(
|
||||
getState().service as FeedbinConfigs,
|
||||
"starred",
|
||||
"DELETE",
|
||||
[parseInt(item.serviceRef)]
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,11 +17,11 @@ export interface FeverConfigs extends ServiceConfigs {
|
|||
useInt32?: boolean
|
||||
}
|
||||
|
||||
async function fetchAPI(configs: FeverConfigs, params="", postparams="") {
|
||||
async function fetchAPI(configs: FeverConfigs, params = "", postparams = "") {
|
||||
const response = await fetch(configs.endpoint + "?api" + params, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/x-www-form-urlencoded" },
|
||||
body: `api_key=${configs.apiKey}${postparams}`
|
||||
body: `api_key=${configs.apiKey}${postparams}`,
|
||||
})
|
||||
return await response.json()
|
||||
}
|
||||
|
@ -29,14 +29,18 @@ async function fetchAPI(configs: FeverConfigs, params="", postparams="") {
|
|||
async function markItem(configs: FeverConfigs, item: RSSItem, as: string) {
|
||||
if (item.serviceRef) {
|
||||
try {
|
||||
await fetchAPI(configs, "", `&mark=item&as=${as}&id=${item.serviceRef}`)
|
||||
await fetchAPI(
|
||||
configs,
|
||||
"",
|
||||
`&mark=item&as=${as}&id=${item.serviceRef}`
|
||||
)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const APIError = () => new Error(intl.get("service.failure"))
|
||||
const APIError = () => new Error(intl.get("service.failure"))
|
||||
|
||||
export const feverServiceHooks: ServiceHooks = {
|
||||
authenticate: async (configs: FeverConfigs) => {
|
||||
|
@ -57,7 +61,8 @@ export const feverServiceHooks: ServiceHooks = {
|
|||
if (configs.importGroups) {
|
||||
// Import groups on the first sync
|
||||
const groups: any[] = (await fetchAPI(configs, "&groups")).groups
|
||||
if (groups === undefined || feedGroups === undefined) throw APIError()
|
||||
if (groups === undefined || feedGroups === undefined)
|
||||
throw APIError()
|
||||
const groupsIdMap = new Map<number, string>()
|
||||
for (let group of groups) {
|
||||
const title = group.title.trim()
|
||||
|
@ -90,7 +95,10 @@ export const feverServiceHooks: ServiceHooks = {
|
|||
response = await fetchAPI(configs, `&items&max_id=${min}`)
|
||||
if (response.items === undefined) throw APIError()
|
||||
items.push(...response.items.filter(i => i.id > configs.lastId))
|
||||
if (response.items.length === 0 && min === Number.MAX_SAFE_INTEGER) {
|
||||
if (
|
||||
response.items.length === 0 &&
|
||||
min === Number.MAX_SAFE_INTEGER
|
||||
) {
|
||||
configs.useInt32 = true
|
||||
min = 2147483647
|
||||
response = undefined
|
||||
|
@ -98,11 +106,14 @@ export const feverServiceHooks: ServiceHooks = {
|
|||
min = response.items.reduce((m, n) => Math.min(m, n.id), min)
|
||||
}
|
||||
} while (
|
||||
min > configs.lastId &&
|
||||
(response === undefined || response.items.length >= 50) &&
|
||||
min > configs.lastId &&
|
||||
(response === undefined || response.items.length >= 50) &&
|
||||
items.length < configs.fetchLimit
|
||||
)
|
||||
configs.lastId = items.reduce((m, n) => Math.max(m, n.id), configs.lastId)
|
||||
configs.lastId = items.reduce(
|
||||
(m, n) => Math.max(m, n.id),
|
||||
configs.lastId
|
||||
)
|
||||
if (items.length > 0) {
|
||||
const fidMap = new Map<string, RSSSource>()
|
||||
for (let source of Object.values(state.sources)) {
|
||||
|
@ -129,27 +140,33 @@ export const feverServiceHooks: ServiceHooks = {
|
|||
} as RSSItem
|
||||
// Try to get the thumbnail of the item
|
||||
let dom = domParser.parseFromString(item.content, "text/html")
|
||||
let baseEl = dom.createElement('base')
|
||||
baseEl.setAttribute('href', item.link.split("/").slice(0, 3).join("/"))
|
||||
let baseEl = dom.createElement("base")
|
||||
baseEl.setAttribute(
|
||||
"href",
|
||||
item.link.split("/").slice(0, 3).join("/")
|
||||
)
|
||||
dom.head.append(baseEl)
|
||||
let img = dom.querySelector("img")
|
||||
if (img && img.src) {
|
||||
if (img && img.src) {
|
||||
item.thumb = img.src
|
||||
} else if (configs.useInt32) { // TTRSS Fever Plugin attachments
|
||||
let a = dom.querySelector("body>ul>li:first-child>a") as HTMLAnchorElement
|
||||
} else if (configs.useInt32) {
|
||||
// TTRSS Fever Plugin attachments
|
||||
let a = dom.querySelector(
|
||||
"body>ul>li:first-child>a"
|
||||
) as HTMLAnchorElement
|
||||
if (a && /, image\/generic$/.test(a.innerText) && a.href)
|
||||
item.thumb = a.href
|
||||
}
|
||||
// Apply rules and sync back to the service
|
||||
if (source.rules) SourceRule.applyAll(source.rules, item)
|
||||
if (Boolean(i.is_read) !== item.hasRead)
|
||||
if (Boolean(i.is_read) !== item.hasRead)
|
||||
markItem(configs, item, item.hasRead ? "read" : "unread")
|
||||
if (Boolean(i.is_saved) !== Boolean(item.starred))
|
||||
if (Boolean(i.is_saved) !== Boolean(item.starred))
|
||||
markItem(configs, item, item.starred ? "saved" : "unsaved")
|
||||
return item
|
||||
})
|
||||
return [parsedItems, configs]
|
||||
} else {
|
||||
} else {
|
||||
return [[], configs]
|
||||
}
|
||||
},
|
||||
|
@ -157,10 +174,13 @@ export const feverServiceHooks: ServiceHooks = {
|
|||
syncItems: () => async (_, getState) => {
|
||||
const configs = getState().service as FeverConfigs
|
||||
const [unreadResponse, starredResponse] = await Promise.all([
|
||||
fetchAPI(configs, "&unread_item_ids"),
|
||||
fetchAPI(configs, "&saved_item_ids")
|
||||
fetchAPI(configs, "&unread_item_ids"),
|
||||
fetchAPI(configs, "&saved_item_ids"),
|
||||
])
|
||||
if (typeof unreadResponse.unread_item_ids !== "string" || typeof starredResponse.saved_item_ids !== "string") {
|
||||
if (
|
||||
typeof unreadResponse.unread_item_ids !== "string" ||
|
||||
typeof starredResponse.saved_item_ids !== "string"
|
||||
) {
|
||||
throw APIError()
|
||||
}
|
||||
const unreadFids: string[] = unreadResponse.unread_item_ids.split(",")
|
||||
|
@ -173,7 +193,9 @@ export const feverServiceHooks: ServiceHooks = {
|
|||
const configs = state.service as FeverConfigs
|
||||
if (date && !before) {
|
||||
const iids = state.feeds[state.page.feedId].iids
|
||||
const items = iids.map(iid => state.items[iid]).filter(i => !i.hasRead && i.date.getTime() >= date.getTime())
|
||||
const items = iids
|
||||
.map(iid => state.items[iid])
|
||||
.filter(i => !i.hasRead && i.date.getTime() >= date.getTime())
|
||||
for (let item of items) {
|
||||
if (item.serviceRef) {
|
||||
markItem(configs, item, "read")
|
||||
|
@ -181,10 +203,15 @@ export const feverServiceHooks: ServiceHooks = {
|
|||
}
|
||||
} else {
|
||||
const sources = sids.map(sid => state.sources[sid])
|
||||
const timestamp = Math.floor((date ? date.getTime() : Date.now()) / 1000) + 1
|
||||
const timestamp =
|
||||
Math.floor((date ? date.getTime() : Date.now()) / 1000) + 1
|
||||
for (let source of sources) {
|
||||
if (source.serviceRef) {
|
||||
fetchAPI(configs, "", `&mark=feed&as=read&id=${source.serviceRef}&before=${timestamp}`)
|
||||
fetchAPI(
|
||||
configs,
|
||||
"",
|
||||
`&mark=feed&as=read&id=${source.serviceRef}&before=${timestamp}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -205,4 +232,4 @@ export const feverServiceHooks: ServiceHooks = {
|
|||
unstar: (item: RSSItem) => async (_, getState) => {
|
||||
await markItem(getState().service as FeverConfigs, item, "unsaved")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,12 @@ export interface GReaderConfigs extends ServiceConfigs {
|
|||
removeInoreaderAd?: boolean
|
||||
}
|
||||
|
||||
async function fetchAPI(configs: GReaderConfigs, params: string, method="GET", body:BodyInit=null) {
|
||||
async function fetchAPI(
|
||||
configs: GReaderConfigs,
|
||||
params: string,
|
||||
method = "GET",
|
||||
body: BodyInit = null
|
||||
) {
|
||||
const headers = new Headers()
|
||||
if (configs.auth !== null) headers.set("Authorization", configs.auth)
|
||||
if (configs.type == SyncService.Inoreader) {
|
||||
|
@ -40,14 +45,17 @@ async function fetchAPI(configs: GReaderConfigs, params: string, method="GET", b
|
|||
headers.set("AppKey", "KPbKYXTfgrKbwmroOeYC7mcW21ZRwF5Y")
|
||||
}
|
||||
}
|
||||
return await fetch(configs.endpoint + params, {
|
||||
method: method,
|
||||
return await fetch(configs.endpoint + params, {
|
||||
method: method,
|
||||
headers: headers,
|
||||
body: body
|
||||
})
|
||||
body: body,
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchAll(configs: GReaderConfigs, params: string): Promise<Set<string>> {
|
||||
async function fetchAll(
|
||||
configs: GReaderConfigs,
|
||||
params: string
|
||||
): Promise<Set<string>> {
|
||||
let results = new Array()
|
||||
let fetched: any[]
|
||||
let continuation: string
|
||||
|
@ -67,8 +75,13 @@ async function fetchAll(configs: GReaderConfigs, params: string): Promise<Set<st
|
|||
return new Set(results)
|
||||
}
|
||||
|
||||
async function editTag(configs: GReaderConfigs, ref: string, tag: string, add=true) {
|
||||
const body = new URLSearchParams(`i=${ref}&${add?"a":"r"}=${tag}`)
|
||||
async function editTag(
|
||||
configs: GReaderConfigs,
|
||||
ref: string,
|
||||
tag: string,
|
||||
add = true
|
||||
) {
|
||||
const body = new URLSearchParams(`i=${ref}&${add ? "a" : "r"}=${tag}`)
|
||||
return await fetchAPI(configs, "/reader/api/0/edit-tag", "POST", body)
|
||||
}
|
||||
|
||||
|
@ -86,7 +99,10 @@ export const gReaderServiceHooks: ServiceHooks = {
|
|||
authenticate: async (configs: GReaderConfigs) => {
|
||||
if (configs.auth !== null) {
|
||||
try {
|
||||
const result = await fetchAPI(configs, "/reader/api/0/user-info")
|
||||
const result = await fetchAPI(
|
||||
configs,
|
||||
"/reader/api/0/user-info"
|
||||
)
|
||||
return result.status === 200
|
||||
} catch {
|
||||
return false
|
||||
|
@ -94,15 +110,23 @@ export const gReaderServiceHooks: ServiceHooks = {
|
|||
}
|
||||
},
|
||||
|
||||
reauthenticate: async (configs: GReaderConfigs): Promise<GReaderConfigs> => {
|
||||
reauthenticate: async (
|
||||
configs: GReaderConfigs
|
||||
): Promise<GReaderConfigs> => {
|
||||
const body = new URLSearchParams()
|
||||
body.append("Email", configs.username)
|
||||
body.append("Passwd", configs.password)
|
||||
const result = await fetchAPI(configs, "/accounts/ClientLogin", "POST", body)
|
||||
const result = await fetchAPI(
|
||||
configs,
|
||||
"/accounts/ClientLogin",
|
||||
"POST",
|
||||
body
|
||||
)
|
||||
if (result.status === 200) {
|
||||
const text = await result.text()
|
||||
const matches = text.match(/Auth=(\S+)/)
|
||||
if (matches.length > 1) configs.auth = "GoogleLogin auth=" + matches[1]
|
||||
if (matches.length > 1)
|
||||
configs.auth = "GoogleLogin auth=" + matches[1]
|
||||
return configs
|
||||
} else {
|
||||
throw APIError()
|
||||
|
@ -111,7 +135,10 @@ export const gReaderServiceHooks: ServiceHooks = {
|
|||
|
||||
updateSources: () => async (dispatch, getState) => {
|
||||
const configs = getState().service as GReaderConfigs
|
||||
const response = await fetchAPI(configs, "/reader/api/0/subscription/list?output=json")
|
||||
const response = await fetchAPI(
|
||||
configs,
|
||||
"/reader/api/0/subscription/list?output=json"
|
||||
)
|
||||
if (response.status !== 200) throw APIError()
|
||||
const subscriptions: any[] = (await response.json()).subscriptions
|
||||
let groupsMap: Map<string, string>
|
||||
|
@ -134,7 +161,10 @@ export const gReaderServiceHooks: ServiceHooks = {
|
|||
const source = new RSSSource(s.url || s.htmlUrl, s.title)
|
||||
source.serviceRef = s.id
|
||||
// Omit duplicate sources in The Old Reader
|
||||
if (configs.useInt64 || s.url != "http://blog.theoldreader.com/rss") {
|
||||
if (
|
||||
configs.useInt64 ||
|
||||
s.url != "http://blog.theoldreader.com/rss"
|
||||
) {
|
||||
sources.push(source)
|
||||
}
|
||||
})
|
||||
|
@ -145,13 +175,25 @@ export const gReaderServiceHooks: ServiceHooks = {
|
|||
const configs = getState().service as GReaderConfigs
|
||||
if (configs.type == SyncService.Inoreader) {
|
||||
return await Promise.all([
|
||||
fetchAll(configs, `/reader/api/0/stream/items/ids?output=json&xt=${READ_TAG}&n=1000`),
|
||||
fetchAll(configs, `/reader/api/0/stream/items/ids?output=json&it=${STAR_TAG}&n=1000`)
|
||||
fetchAll(
|
||||
configs,
|
||||
`/reader/api/0/stream/items/ids?output=json&xt=${READ_TAG}&n=1000`
|
||||
),
|
||||
fetchAll(
|
||||
configs,
|
||||
`/reader/api/0/stream/items/ids?output=json&it=${STAR_TAG}&n=1000`
|
||||
),
|
||||
])
|
||||
} else {
|
||||
return await Promise.all([
|
||||
fetchAll(configs, `/reader/api/0/stream/items/ids?output=json&s=${ALL_TAG}&xt=${READ_TAG}&n=1000`),
|
||||
fetchAll(configs, `/reader/api/0/stream/items/ids?output=json&s=${STAR_TAG}&n=1000`)
|
||||
fetchAll(
|
||||
configs,
|
||||
`/reader/api/0/stream/items/ids?output=json&s=${ALL_TAG}&xt=${READ_TAG}&n=1000`
|
||||
),
|
||||
fetchAll(
|
||||
configs,
|
||||
`/reader/api/0/stream/items/ids?output=json&s=${STAR_TAG}&n=1000`
|
||||
),
|
||||
])
|
||||
}
|
||||
},
|
||||
|
@ -173,7 +215,10 @@ export const gReaderServiceHooks: ServiceHooks = {
|
|||
fetchedItems = fetched.items
|
||||
for (let i of fetchedItems) {
|
||||
i.id = compactId(i.id, configs.useInt64)
|
||||
if (i.id === configs.lastId || items.length >= configs.fetchLimit) {
|
||||
if (
|
||||
i.id === configs.lastId ||
|
||||
items.length >= configs.fetchLimit
|
||||
) {
|
||||
break
|
||||
} else {
|
||||
items.push(i)
|
||||
|
@ -196,9 +241,19 @@ export const gReaderServiceHooks: ServiceHooks = {
|
|||
items.map(i => {
|
||||
const source = fidMap.get(i.origin.streamId)
|
||||
if (source === undefined) return
|
||||
const dom = domParser.parseFromString(i.summary.content, "text/html")
|
||||
if (configs.type == SyncService.Inoreader && configs.removeInoreaderAd !== false) {
|
||||
if (dom.documentElement.textContent.trim().startsWith("Ads from Inoreader")) {
|
||||
const dom = domParser.parseFromString(
|
||||
i.summary.content,
|
||||
"text/html"
|
||||
)
|
||||
if (
|
||||
configs.type == SyncService.Inoreader &&
|
||||
configs.removeInoreaderAd !== false
|
||||
) {
|
||||
if (
|
||||
dom.documentElement.textContent
|
||||
.trim()
|
||||
.startsWith("Ads from Inoreader")
|
||||
) {
|
||||
dom.body.firstChild.remove()
|
||||
}
|
||||
}
|
||||
|
@ -215,17 +270,26 @@ export const gReaderServiceHooks: ServiceHooks = {
|
|||
starred: false,
|
||||
hidden: false,
|
||||
notify: false,
|
||||
serviceRef: i.id
|
||||
serviceRef: i.id,
|
||||
} as RSSItem
|
||||
const baseEl = dom.createElement('base')
|
||||
baseEl.setAttribute('href', item.link.split("/").slice(0, 3).join("/"))
|
||||
const baseEl = dom.createElement("base")
|
||||
baseEl.setAttribute(
|
||||
"href",
|
||||
item.link.split("/").slice(0, 3).join("/")
|
||||
)
|
||||
dom.head.append(baseEl)
|
||||
let img = dom.querySelector("img")
|
||||
if (img && img.src) item.thumb = img.src
|
||||
if (configs.type == SyncService.Inoreader) item.title = htmlDecode(item.title)
|
||||
if (configs.type == SyncService.Inoreader)
|
||||
item.title = htmlDecode(item.title)
|
||||
for (let c of i.categories) {
|
||||
if (!item.hasRead && c.endsWith("/state/com.google/read")) item.hasRead = true
|
||||
else if (!item.starred && c.endsWith("/state/com.google/starred")) item.starred = true
|
||||
if (!item.hasRead && c.endsWith("/state/com.google/read"))
|
||||
item.hasRead = true
|
||||
else if (
|
||||
!item.starred &&
|
||||
c.endsWith("/state/com.google/starred")
|
||||
)
|
||||
item.starred = true
|
||||
}
|
||||
// Apply rules and sync back to the service
|
||||
if (source.rules) {
|
||||
|
@ -233,14 +297,26 @@ export const gReaderServiceHooks: ServiceHooks = {
|
|||
const starred = item.starred
|
||||
SourceRule.applyAll(source.rules, item)
|
||||
if (item.hasRead !== hasRead)
|
||||
editTag(configs, item.serviceRef, READ_TAG, item.hasRead)
|
||||
editTag(
|
||||
configs,
|
||||
item.serviceRef,
|
||||
READ_TAG,
|
||||
item.hasRead
|
||||
)
|
||||
if (item.starred !== starred)
|
||||
editTag(configs, item.serviceRef, STAR_TAG, item.starred)
|
||||
}
|
||||
editTag(
|
||||
configs,
|
||||
item.serviceRef,
|
||||
STAR_TAG,
|
||||
item.starred
|
||||
)
|
||||
}
|
||||
parsedItems.push(item)
|
||||
})
|
||||
if (parsedItems.length > 0) {
|
||||
configs.lastFetched = Math.round(parsedItems[0].fetchedDate.getTime() / 1000)
|
||||
configs.lastFetched = Math.round(
|
||||
parsedItems[0].fetchedDate.getTime() / 1000
|
||||
)
|
||||
}
|
||||
return [parsedItems, configs]
|
||||
} else {
|
||||
|
@ -255,13 +331,19 @@ export const gReaderServiceHooks: ServiceHooks = {
|
|||
const predicates: lf.Predicate[] = [
|
||||
db.items.source.in(sids),
|
||||
db.items.hasRead.eq(false),
|
||||
db.items.serviceRef.isNotNull()
|
||||
db.items.serviceRef.isNotNull(),
|
||||
]
|
||||
if (date) {
|
||||
predicates.push(before ? db.items.date.lte(date) : db.items.date.gte(date))
|
||||
predicates.push(
|
||||
before ? db.items.date.lte(date) : db.items.date.gte(date)
|
||||
)
|
||||
}
|
||||
const query = lf.op.and.apply(null, predicates)
|
||||
const rows = await db.itemsDB.select(db.items.serviceRef).from(db.items).where(query).exec()
|
||||
const rows = await db.itemsDB
|
||||
.select(db.items.serviceRef)
|
||||
.from(db.items)
|
||||
.where(query)
|
||||
.exec()
|
||||
const refs = rows.map(row => row["serviceRef"]).join("&i=")
|
||||
if (refs) {
|
||||
editTag(getState().service as GReaderConfigs, refs, READ_TAG)
|
||||
|
@ -272,25 +354,48 @@ export const gReaderServiceHooks: ServiceHooks = {
|
|||
if (source.serviceRef) {
|
||||
const body = new URLSearchParams()
|
||||
body.set("s", source.serviceRef)
|
||||
fetchAPI(configs, "/reader/api/0/mark-all-as-read", "POST", body)
|
||||
fetchAPI(
|
||||
configs,
|
||||
"/reader/api/0/mark-all-as-read",
|
||||
"POST",
|
||||
body
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
markRead: (item: RSSItem) => async (_, getState) => {
|
||||
await editTag(getState().service as GReaderConfigs, item.serviceRef, READ_TAG)
|
||||
await editTag(
|
||||
getState().service as GReaderConfigs,
|
||||
item.serviceRef,
|
||||
READ_TAG
|
||||
)
|
||||
},
|
||||
|
||||
markUnread: (item: RSSItem) => async (_, getState) => {
|
||||
await editTag(getState().service as GReaderConfigs, item.serviceRef, READ_TAG, false)
|
||||
await editTag(
|
||||
getState().service as GReaderConfigs,
|
||||
item.serviceRef,
|
||||
READ_TAG,
|
||||
false
|
||||
)
|
||||
},
|
||||
|
||||
star: (item: RSSItem) => async (_, getState) => {
|
||||
await editTag(getState().service as GReaderConfigs, item.serviceRef, STAR_TAG)
|
||||
await editTag(
|
||||
getState().service as GReaderConfigs,
|
||||
item.serviceRef,
|
||||
STAR_TAG
|
||||
)
|
||||
},
|
||||
|
||||
unstar: (item: RSSItem) => async (_, getState) => {
|
||||
await editTag(getState().service as GReaderConfigs, item.serviceRef, STAR_TAG, false)
|
||||
await editTag(
|
||||
getState().service as GReaderConfigs,
|
||||
item.serviceRef,
|
||||
STAR_TAG,
|
||||
false
|
||||
)
|
||||
},
|
||||
}
|
||||
|
|
|
@ -3,13 +3,24 @@ import intl from "react-intl-universal"
|
|||
import * as db from "../db"
|
||||
import lf from "lovefield"
|
||||
import { fetchFavicon, ActionStatus, AppThunk, parseRSS } from "../utils"
|
||||
import { RSSItem, insertItems, ItemActionTypes, FETCH_ITEMS, MARK_READ, MARK_UNREAD, MARK_ALL_READ } from "./item"
|
||||
import {
|
||||
RSSItem,
|
||||
insertItems,
|
||||
ItemActionTypes,
|
||||
FETCH_ITEMS,
|
||||
MARK_READ,
|
||||
MARK_UNREAD,
|
||||
MARK_ALL_READ,
|
||||
} from "./item"
|
||||
import { saveSettings } from "./app"
|
||||
import { SourceRule } from "./rule"
|
||||
import { fixBrokenGroups } from "./group"
|
||||
|
||||
export const enum SourceOpenTarget {
|
||||
Local, Webpage, External, FullContent
|
||||
Local,
|
||||
Webpage,
|
||||
External,
|
||||
FullContent,
|
||||
}
|
||||
|
||||
export class RSSSource {
|
||||
|
@ -41,13 +52,23 @@ export class RSSSource {
|
|||
return feed
|
||||
}
|
||||
|
||||
private static async checkItem(source: RSSSource, item: Parser.Item): Promise<RSSItem> {
|
||||
private static async checkItem(
|
||||
source: RSSSource,
|
||||
item: Parser.Item
|
||||
): Promise<RSSItem> {
|
||||
let i = new RSSItem(item, source)
|
||||
const items = (await db.itemsDB.select().from(db.items).where(lf.op.and(
|
||||
db.items.source.eq(i.source),
|
||||
db.items.title.eq(i.title),
|
||||
db.items.date.eq(i.date)
|
||||
)).limit(1).exec()) as RSSItem[]
|
||||
const items = (await db.itemsDB
|
||||
.select()
|
||||
.from(db.items)
|
||||
.where(
|
||||
lf.op.and(
|
||||
db.items.source.eq(i.source),
|
||||
db.items.title.eq(i.title),
|
||||
db.items.date.eq(i.date)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
.exec()) as RSSItem[]
|
||||
if (items.length === 0) {
|
||||
RSSItem.parseContent(i, item)
|
||||
if (source.rules) SourceRule.applyAll(source.rules, i)
|
||||
|
@ -57,15 +78,22 @@ export class RSSSource {
|
|||
}
|
||||
}
|
||||
|
||||
static checkItems(source: RSSSource, items: Parser.Item[]): Promise<RSSItem[]> {
|
||||
static checkItems(
|
||||
source: RSSSource,
|
||||
items: Parser.Item[]
|
||||
): Promise<RSSItem[]> {
|
||||
return new Promise<RSSItem[]>((resolve, reject) => {
|
||||
let p = new Array<Promise<RSSItem>>()
|
||||
for (let item of items) {
|
||||
p.push(this.checkItem(source, item))
|
||||
}
|
||||
Promise.all(p).then(values => {
|
||||
resolve(values.filter(v => v != null))
|
||||
}).catch(e => { reject(e) })
|
||||
Promise.all(p)
|
||||
.then(values => {
|
||||
resolve(values.filter(v => v != null))
|
||||
})
|
||||
.catch(e => {
|
||||
reject(e)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -111,17 +139,21 @@ interface UpdateUnreadCountsAction {
|
|||
}
|
||||
|
||||
interface DeleteSourceAction {
|
||||
type: typeof DELETE_SOURCE,
|
||||
type: typeof DELETE_SOURCE
|
||||
source: RSSSource
|
||||
}
|
||||
|
||||
export type SourceActionTypes = InitSourcesAction | AddSourceAction | UpdateSourceAction
|
||||
| UpdateUnreadCountsAction | DeleteSourceAction
|
||||
export type SourceActionTypes =
|
||||
| InitSourcesAction
|
||||
| AddSourceAction
|
||||
| UpdateSourceAction
|
||||
| UpdateUnreadCountsAction
|
||||
| DeleteSourceAction
|
||||
|
||||
export function initSourcesRequest(): SourceActionTypes {
|
||||
return {
|
||||
type: INIT_SOURCES,
|
||||
status: ActionStatus.Request
|
||||
status: ActionStatus.Request,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -129,7 +161,7 @@ export function initSourcesSuccess(sources: SourceState): SourceActionTypes {
|
|||
return {
|
||||
type: INIT_SOURCES,
|
||||
status: ActionStatus.Success,
|
||||
sources: sources
|
||||
sources: sources,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -137,19 +169,17 @@ export function initSourcesFailure(err): SourceActionTypes {
|
|||
return {
|
||||
type: INIT_SOURCES,
|
||||
status: ActionStatus.Failure,
|
||||
err: err
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
async function unreadCount(sources: SourceState): Promise<SourceState> {
|
||||
const rows = await db.itemsDB.select(
|
||||
db.items.source,
|
||||
lf.fn.count(db.items._id)
|
||||
).from(db.items).where(
|
||||
db.items.hasRead.eq(false)
|
||||
).groupBy(
|
||||
db.items.source
|
||||
).exec()
|
||||
const rows = await db.itemsDB
|
||||
.select(db.items.source, lf.fn.count(db.items._id))
|
||||
.from(db.items)
|
||||
.where(db.items.hasRead.eq(false))
|
||||
.groupBy(db.items.source)
|
||||
.exec()
|
||||
for (let row of rows) {
|
||||
sources[row["source"]].unreadCount = row["COUNT(_id)"]
|
||||
}
|
||||
|
@ -162,7 +192,7 @@ export function updateUnreadCounts(): AppThunk<Promise<void>> {
|
|||
for (let source of Object.values(getState().sources)) {
|
||||
sources[source.sid] = {
|
||||
...source,
|
||||
unreadCount: 0
|
||||
unreadCount: 0,
|
||||
}
|
||||
}
|
||||
dispatch({
|
||||
|
@ -173,10 +203,13 @@ export function updateUnreadCounts(): AppThunk<Promise<void>> {
|
|||
}
|
||||
|
||||
export function initSources(): AppThunk<Promise<void>> {
|
||||
return async (dispatch) => {
|
||||
return async dispatch => {
|
||||
dispatch(initSourcesRequest())
|
||||
await db.init()
|
||||
const sources = (await db.sourcesDB.select().from(db.sources).exec()) as RSSSource[]
|
||||
const sources = (await db.sourcesDB
|
||||
.select()
|
||||
.from(db.sources)
|
||||
.exec()) as RSSSource[]
|
||||
const state: SourceState = {}
|
||||
for (let source of sources) {
|
||||
source.unreadCount = 0
|
||||
|
@ -192,16 +225,19 @@ export function addSourceRequest(batch: boolean): SourceActionTypes {
|
|||
return {
|
||||
type: ADD_SOURCE,
|
||||
batch: batch,
|
||||
status: ActionStatus.Request
|
||||
status: ActionStatus.Request,
|
||||
}
|
||||
}
|
||||
|
||||
export function addSourceSuccess(source: RSSSource, batch: boolean): SourceActionTypes {
|
||||
export function addSourceSuccess(
|
||||
source: RSSSource,
|
||||
batch: boolean
|
||||
): SourceActionTypes {
|
||||
return {
|
||||
type: ADD_SOURCE,
|
||||
batch: batch,
|
||||
status: ActionStatus.Success,
|
||||
source: source
|
||||
source: source,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -210,7 +246,7 @@ export function addSourceFailure(err, batch: boolean): SourceActionTypes {
|
|||
type: ADD_SOURCE,
|
||||
batch: batch,
|
||||
status: ActionStatus.Failure,
|
||||
err: err
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -223,7 +259,11 @@ export function insertSource(source: RSSSource): AppThunk<Promise<RSSSource>> {
|
|||
source.sid = Math.max(...sids, -1) + 1
|
||||
const row = db.sources.createRow(source)
|
||||
try {
|
||||
const inserted = (await db.sourcesDB.insert().into(db.sources).values([row]).exec()) as RSSSource[]
|
||||
const inserted = (await db.sourcesDB
|
||||
.insert()
|
||||
.into(db.sources)
|
||||
.values([row])
|
||||
.exec()) as RSSSource[]
|
||||
resolve(inserted[0])
|
||||
} catch (err) {
|
||||
if (err.code === 201) reject(intl.get("sources.exist"))
|
||||
|
@ -234,7 +274,11 @@ export function insertSource(source: RSSSource): AppThunk<Promise<RSSSource>> {
|
|||
}
|
||||
}
|
||||
|
||||
export function addSource(url: string, name: string = null, batch = false): AppThunk<Promise<number>> {
|
||||
export function addSource(
|
||||
url: string,
|
||||
name: string = null,
|
||||
batch = false
|
||||
): AppThunk<Promise<number>> {
|
||||
return async (dispatch, getState) => {
|
||||
const app = getState().app
|
||||
if (app.sourceInit) {
|
||||
|
@ -253,7 +297,10 @@ export function addSource(url: string, name: string = null, batch = false): AppT
|
|||
} catch (e) {
|
||||
dispatch(addSourceFailure(e, batch))
|
||||
if (!batch) {
|
||||
window.utils.showErrorBox(intl.get("sources.errorAdd"), String(e))
|
||||
window.utils.showErrorBox(
|
||||
intl.get("sources.errorAdd"),
|
||||
String(e)
|
||||
)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
|
@ -265,16 +312,20 @@ export function addSource(url: string, name: string = null, batch = false): AppT
|
|||
export function updateSourceDone(source: RSSSource): SourceActionTypes {
|
||||
return {
|
||||
type: UPDATE_SOURCE,
|
||||
source: source
|
||||
source: source,
|
||||
}
|
||||
}
|
||||
|
||||
export function updateSource(source: RSSSource): AppThunk<Promise<void>> {
|
||||
return async (dispatch) => {
|
||||
return async dispatch => {
|
||||
let sourceCopy = { ...source }
|
||||
delete sourceCopy.unreadCount
|
||||
const row = db.sources.createRow(sourceCopy)
|
||||
await db.sourcesDB.insertOrReplace().into(db.sources).values([row]).exec()
|
||||
await db.sourcesDB
|
||||
.insertOrReplace()
|
||||
.into(db.sources)
|
||||
.values([row])
|
||||
.exec()
|
||||
dispatch(updateSourceDone(source))
|
||||
}
|
||||
}
|
||||
|
@ -282,16 +333,27 @@ export function updateSource(source: RSSSource): AppThunk<Promise<void>> {
|
|||
export function deleteSourceDone(source: RSSSource): SourceActionTypes {
|
||||
return {
|
||||
type: DELETE_SOURCE,
|
||||
source: source
|
||||
source: source,
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteSource(source: RSSSource, batch = false): AppThunk<Promise<void>> {
|
||||
export function deleteSource(
|
||||
source: RSSSource,
|
||||
batch = false
|
||||
): AppThunk<Promise<void>> {
|
||||
return async (dispatch, getState) => {
|
||||
if (!batch) dispatch(saveSettings())
|
||||
try {
|
||||
await db.itemsDB.delete().from(db.items).where(db.items.source.eq(source.sid)).exec()
|
||||
await db.sourcesDB.delete().from(db.sources).where(db.sources.sid.eq(source.sid)).exec()
|
||||
await db.itemsDB
|
||||
.delete()
|
||||
.from(db.items)
|
||||
.where(db.items.source.eq(source.sid))
|
||||
.exec()
|
||||
await db.sourcesDB
|
||||
.delete()
|
||||
.from(db.sources)
|
||||
.where(db.sources.sid.eq(source.sid))
|
||||
.exec()
|
||||
dispatch(deleteSourceDone(source))
|
||||
window.settings.saveGroups(getState().groups)
|
||||
} catch (err) {
|
||||
|
@ -303,7 +365,7 @@ export function deleteSource(source: RSSSource, batch = false): AppThunk<Promise
|
|||
}
|
||||
|
||||
export function deleteSources(sources: RSSSource[]): AppThunk<Promise<void>> {
|
||||
return async (dispatch) => {
|
||||
return async dispatch => {
|
||||
dispatch(saveSettings())
|
||||
for (let source of sources) {
|
||||
await dispatch(deleteSource(source, true))
|
||||
|
@ -312,11 +374,16 @@ export function deleteSources(sources: RSSSource[]): AppThunk<Promise<void>> {
|
|||
}
|
||||
}
|
||||
|
||||
export function updateFavicon(sids?: number[], force=false): AppThunk<Promise<void>> {
|
||||
export function updateFavicon(
|
||||
sids?: number[],
|
||||
force = false
|
||||
): AppThunk<Promise<void>> {
|
||||
return async (dispatch, getState) => {
|
||||
const initSources = getState().sources
|
||||
if (!sids) {
|
||||
sids = Object.values(initSources).filter(s => s.iconurl === undefined).map(s => s.sid)
|
||||
sids = Object.values(initSources)
|
||||
.filter(s => s.iconurl === undefined)
|
||||
.map(s => s.sid)
|
||||
} else {
|
||||
sids = sids.filter(sid => sid in initSources)
|
||||
}
|
||||
|
@ -324,7 +391,11 @@ export function updateFavicon(sids?: number[], force=false): AppThunk<Promise<vo
|
|||
const url = initSources[sid].url
|
||||
let favicon = (await fetchFavicon(url)) || ""
|
||||
const source = getState().sources[sid]
|
||||
if (source && source.url === url && (force || source.iconurl === undefined)) {
|
||||
if (
|
||||
source &&
|
||||
source.url === url &&
|
||||
(force || source.iconurl === undefined)
|
||||
) {
|
||||
source.iconurl = favicon
|
||||
await dispatch(updateSource(source))
|
||||
}
|
||||
|
@ -340,22 +411,28 @@ export function sourceReducer(
|
|||
switch (action.type) {
|
||||
case INIT_SOURCES:
|
||||
switch (action.status) {
|
||||
case ActionStatus.Success: return action.sources
|
||||
default: return state
|
||||
case ActionStatus.Success:
|
||||
return action.sources
|
||||
default:
|
||||
return state
|
||||
}
|
||||
case UPDATE_UNREAD_COUNTS: return action.sources
|
||||
case UPDATE_UNREAD_COUNTS:
|
||||
return action.sources
|
||||
case ADD_SOURCE:
|
||||
switch (action.status) {
|
||||
case ActionStatus.Success: return {
|
||||
...state,
|
||||
[action.source.sid]: action.source
|
||||
}
|
||||
default: return state
|
||||
case ActionStatus.Success:
|
||||
return {
|
||||
...state,
|
||||
[action.source.sid]: action.source,
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
case UPDATE_SOURCE:
|
||||
return {
|
||||
...state,
|
||||
[action.source.sid]: action.source,
|
||||
}
|
||||
case UPDATE_SOURCE: return {
|
||||
...state,
|
||||
[action.source.sid]: action.source
|
||||
}
|
||||
case DELETE_SOURCE: {
|
||||
delete state[action.source.sid]
|
||||
return { ...state }
|
||||
|
@ -365,10 +442,14 @@ export function sourceReducer(
|
|||
case ActionStatus.Success: {
|
||||
let updateMap = new Map<number, number>()
|
||||
for (let item of action.items) {
|
||||
if (!item.hasRead) { updateMap.set(
|
||||
item.source,
|
||||
updateMap.has(item.source) ? (updateMap.get(item.source) + 1) : 1
|
||||
)}
|
||||
if (!item.hasRead) {
|
||||
updateMap.set(
|
||||
item.source,
|
||||
updateMap.has(item.source)
|
||||
? updateMap.get(item.source) + 1
|
||||
: 1
|
||||
)
|
||||
}
|
||||
}
|
||||
let nextState = {} as SourceState
|
||||
for (let [s, source] of Object.entries(state)) {
|
||||
|
@ -376,7 +457,8 @@ export function sourceReducer(
|
|||
if (updateMap.has(sid)) {
|
||||
nextState[sid] = {
|
||||
...source,
|
||||
unreadCount: source.unreadCount + updateMap.get(sid)
|
||||
unreadCount:
|
||||
source.unreadCount + updateMap.get(sid),
|
||||
} as RSSSource
|
||||
} else {
|
||||
nextState[sid] = source
|
||||
|
@ -384,27 +466,32 @@ export function sourceReducer(
|
|||
}
|
||||
return nextState
|
||||
}
|
||||
default: return state
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
case MARK_UNREAD:
|
||||
case MARK_READ: return {
|
||||
...state,
|
||||
[action.item.source]: {
|
||||
...state[action.item.source],
|
||||
unreadCount: state[action.item.source].unreadCount + (action.type === MARK_UNREAD ? 1 : -1)
|
||||
} as RSSSource
|
||||
}
|
||||
case MARK_READ:
|
||||
return {
|
||||
...state,
|
||||
[action.item.source]: {
|
||||
...state[action.item.source],
|
||||
unreadCount:
|
||||
state[action.item.source].unreadCount +
|
||||
(action.type === MARK_UNREAD ? 1 : -1),
|
||||
} as RSSSource,
|
||||
}
|
||||
case MARK_ALL_READ: {
|
||||
let nextState = { ...state }
|
||||
action.sids.map((sid, i) => {
|
||||
nextState[sid] = {
|
||||
...state[sid],
|
||||
unreadCount: action.time ? state[sid].unreadCount : 0
|
||||
unreadCount: action.time ? state[sid].unreadCount : 0,
|
||||
}
|
||||
})
|
||||
return nextState
|
||||
}
|
||||
default: return state
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ export const rootReducer = combineReducers({
|
|||
groups: groupReducer,
|
||||
page: pageReducer,
|
||||
service: serviceReducer,
|
||||
app: appReducer
|
||||
app: appReducer,
|
||||
})
|
||||
|
||||
export type RootState = ReturnType<typeof rootReducer>
|
||||
export type RootState = ReturnType<typeof rootReducer>
|
||||
|
|
|
@ -5,7 +5,10 @@ import { ThemeSettings } from "../schema-types"
|
|||
import intl from "react-intl-universal"
|
||||
|
||||
const lightTheme: IPartialTheme = {
|
||||
defaultFontStyle: { fontFamily: '"Segoe UI", "Source Han Sans SC Regular", "Microsoft YaHei", sans-serif' }
|
||||
defaultFontStyle: {
|
||||
fontFamily:
|
||||
'"Segoe UI", "Source Han Sans SC Regular", "Microsoft YaHei", sans-serif',
|
||||
},
|
||||
}
|
||||
const darkTheme: IPartialTheme = {
|
||||
...lightTheme,
|
||||
|
@ -33,8 +36,8 @@ const darkTheme: IPartialTheme = {
|
|||
themeDarkAlt: "#4ba0e1",
|
||||
themeDark: "#65aee6",
|
||||
themeDarker: "#8ac2ec",
|
||||
accent: "#3a96dd"
|
||||
}
|
||||
accent: "#3a96dd",
|
||||
},
|
||||
}
|
||||
|
||||
export function setThemeSettings(theme: ThemeSettings) {
|
||||
|
@ -47,7 +50,7 @@ export function getThemeSettings(): ThemeSettings {
|
|||
export function applyThemeSettings() {
|
||||
loadTheme(window.settings.shouldUseDarkColors() ? darkTheme : lightTheme)
|
||||
}
|
||||
window.settings.addThemeUpdateListener((shouldDark) => {
|
||||
window.settings.addThemeUpdateListener(shouldDark => {
|
||||
loadTheme(shouldDark ? darkTheme : lightTheme)
|
||||
})
|
||||
|
||||
|
@ -55,12 +58,15 @@ export function getCurrentLocale() {
|
|||
let locale = window.settings.getCurrentLocale()
|
||||
if (locale in locales) return locale
|
||||
locale = locale.split("-")[0]
|
||||
return (locale in locales) ? locale : "en-US"
|
||||
return locale in locales ? locale : "en-US"
|
||||
}
|
||||
|
||||
export async function exportAll() {
|
||||
const filters = [{ name: intl.get("app.frData"), extensions: ["frdata"] }]
|
||||
const write = await window.utils.showSaveDialog(filters, "*/Fluent_Reader_Backup.frdata")
|
||||
const write = await window.utils.showSaveDialog(
|
||||
filters,
|
||||
"*/Fluent_Reader_Backup.frdata"
|
||||
)
|
||||
if (write) {
|
||||
let output = window.settings.getAll()
|
||||
output["lovefield"] = {
|
||||
|
@ -78,8 +84,10 @@ export async function importAll() {
|
|||
let confirmed = await window.utils.showMessageBox(
|
||||
intl.get("app.restore"),
|
||||
intl.get("app.confirmImport"),
|
||||
intl.get("confirm"), intl.get("cancel"),
|
||||
true, "warning"
|
||||
intl.get("confirm"),
|
||||
intl.get("cancel"),
|
||||
true,
|
||||
"warning"
|
||||
)
|
||||
if (!confirmed) return true
|
||||
let configs = JSON.parse(data)
|
||||
|
@ -90,14 +98,19 @@ export async function importAll() {
|
|||
configs.useNeDB = true
|
||||
openRequest.onsuccess = () => {
|
||||
let db = openRequest.result
|
||||
let objectStore = db.transaction("nedbdata", "readwrite").objectStore("nedbdata")
|
||||
let objectStore = db
|
||||
.transaction("nedbdata", "readwrite")
|
||||
.objectStore("nedbdata")
|
||||
let requests = Object.entries(configs.nedb).map(([key, value]) => {
|
||||
return objectStore.put(value, key)
|
||||
})
|
||||
let promises = requests.map(req => new Promise<void>((resolve, reject) => {
|
||||
req.onsuccess = () => resolve()
|
||||
req.onerror = () => reject()
|
||||
}))
|
||||
let promises = requests.map(
|
||||
req =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
req.onsuccess = () => resolve()
|
||||
req.onerror = () => reject()
|
||||
})
|
||||
)
|
||||
Promise.all(promises).then(() => {
|
||||
delete configs.nedb
|
||||
window.settings.setAll(configs)
|
||||
|
@ -117,6 +130,6 @@ export async function importAll() {
|
|||
await db.itemsDB.insert().into(db.items).values(iRows).exec()
|
||||
delete configs.lovefield
|
||||
window.settings.setAll(configs)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -7,14 +7,17 @@ import Url from "url"
|
|||
import { SearchEngines } from "../schema-types"
|
||||
|
||||
export enum ActionStatus {
|
||||
Request, Success, Failure, Intermediate
|
||||
Request,
|
||||
Success,
|
||||
Failure,
|
||||
Intermediate,
|
||||
}
|
||||
|
||||
export type AppThunk<ReturnType = void> = ThunkAction<
|
||||
ReturnType,
|
||||
RootState,
|
||||
unknown,
|
||||
AnyAction
|
||||
ReturnType,
|
||||
RootState,
|
||||
unknown,
|
||||
AnyAction
|
||||
>
|
||||
|
||||
export type AppDispatch = ThunkDispatch<RootState, undefined, AnyAction>
|
||||
|
@ -22,32 +25,47 @@ export type AppDispatch = ThunkDispatch<RootState, undefined, AnyAction>
|
|||
const rssParser = new Parser({
|
||||
customFields: {
|
||||
item: [
|
||||
"thumb", "image", ["content:encoded", "fullContent"],
|
||||
['media:content', 'mediaContent', {keepArray: true}],
|
||||
] as Parser.CustomFieldItem[]
|
||||
}
|
||||
"thumb",
|
||||
"image",
|
||||
["content:encoded", "fullContent"],
|
||||
["media:content", "mediaContent", { keepArray: true }],
|
||||
] as Parser.CustomFieldItem[],
|
||||
},
|
||||
})
|
||||
|
||||
const CHARSET_RE = /charset=([^()<>@,;:\"/[\]?.=\s]*)/i
|
||||
const XML_ENCODING_RE = /^<\?xml.+encoding="(.+?)".*?\?>/i
|
||||
export async function decodeFetchResponse(response: Response, isHTML = false) {
|
||||
const buffer = await response.arrayBuffer()
|
||||
let ctype = response.headers.has("content-type") && response.headers.get("content-type")
|
||||
let charset = (ctype && CHARSET_RE.test(ctype)) ? CHARSET_RE.exec(ctype)[1] : undefined
|
||||
let content = (new TextDecoder(charset)).decode(buffer)
|
||||
let ctype =
|
||||
response.headers.has("content-type") &&
|
||||
response.headers.get("content-type")
|
||||
let charset =
|
||||
ctype && CHARSET_RE.test(ctype) ? CHARSET_RE.exec(ctype)[1] : undefined
|
||||
let content = new TextDecoder(charset).decode(buffer)
|
||||
if (charset === undefined) {
|
||||
if (isHTML) {
|
||||
const dom = domParser.parseFromString(content, "text/html")
|
||||
charset = dom.querySelector("meta[charset]")?.getAttribute("charset")?.toLowerCase()
|
||||
charset = dom
|
||||
.querySelector("meta[charset]")
|
||||
?.getAttribute("charset")
|
||||
?.toLowerCase()
|
||||
if (!charset) {
|
||||
ctype = dom.querySelector("meta[http-equiv='Content-Type']")?.getAttribute("content")
|
||||
charset = ctype && CHARSET_RE.test(ctype) && CHARSET_RE.exec(ctype)[1].toLowerCase()
|
||||
ctype = dom
|
||||
.querySelector("meta[http-equiv='Content-Type']")
|
||||
?.getAttribute("content")
|
||||
charset =
|
||||
ctype &&
|
||||
CHARSET_RE.test(ctype) &&
|
||||
CHARSET_RE.exec(ctype)[1].toLowerCase()
|
||||
}
|
||||
} else {
|
||||
charset = XML_ENCODING_RE.test(content) && XML_ENCODING_RE.exec(content)[1].toLowerCase()
|
||||
charset =
|
||||
XML_ENCODING_RE.test(content) &&
|
||||
XML_ENCODING_RE.exec(content)[1].toLowerCase()
|
||||
}
|
||||
if (charset && charset !== "utf-8" && charset !== "utf8") {
|
||||
content = (new TextDecoder(charset)).decode(buffer)
|
||||
content = new TextDecoder(charset).decode(buffer)
|
||||
}
|
||||
}
|
||||
return content
|
||||
|
@ -62,7 +80,9 @@ export async function parseRSS(url: string) {
|
|||
}
|
||||
if (result && result.ok) {
|
||||
try {
|
||||
return await rssParser.parseString(await decodeFetchResponse(result))
|
||||
return await rssParser.parseString(
|
||||
await decodeFetchResponse(result)
|
||||
)
|
||||
} catch {
|
||||
throw new Error(intl.get("log.parseError"))
|
||||
}
|
||||
|
@ -83,7 +103,10 @@ export async function fetchFavicon(url: string) {
|
|||
let links = dom.getElementsByTagName("link")
|
||||
for (let link of links) {
|
||||
let rel = link.getAttribute("rel")
|
||||
if ((rel === "icon" || rel === "shortcut icon") && link.hasAttribute("href")) {
|
||||
if (
|
||||
(rel === "icon" || rel === "shortcut icon") &&
|
||||
link.hasAttribute("href")
|
||||
) {
|
||||
let href = link.getAttribute("href")
|
||||
let parsedUrl = Url.parse(url)
|
||||
if (href.startsWith("//")) return parsedUrl.protocol + href
|
||||
|
@ -93,7 +116,7 @@ export async function fetchFavicon(url: string) {
|
|||
}
|
||||
}
|
||||
url = url + "/favicon.ico"
|
||||
if (await validateFavicon(url)) {
|
||||
if (await validateFavicon(url)) {
|
||||
return url
|
||||
} else {
|
||||
return null
|
||||
|
@ -107,8 +130,11 @@ export async function validateFavicon(url: string) {
|
|||
let flag = false
|
||||
try {
|
||||
const result = await fetch(url, { credentials: "omit" })
|
||||
if (result.status == 200 && result.headers.has("Content-Type")
|
||||
&& result.headers.get("Content-Type").startsWith("image")) {
|
||||
if (
|
||||
result.status == 200 &&
|
||||
result.headers.has("Content-Type") &&
|
||||
result.headers.get("Content-Type").startsWith("image")
|
||||
) {
|
||||
flag = true
|
||||
}
|
||||
} finally {
|
||||
|
@ -121,41 +147,55 @@ export function htmlDecode(input: string) {
|
|||
return doc.documentElement.textContent
|
||||
}
|
||||
|
||||
export const urlTest = (s: string) =>
|
||||
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,63}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi.test(s)
|
||||
export const urlTest = (s: string) =>
|
||||
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,63}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi.test(
|
||||
s
|
||||
)
|
||||
|
||||
export const getWindowBreakpoint = () => window.outerWidth >= 1440
|
||||
|
||||
export const cutText = (s: string, length: number) => {
|
||||
return (s.length <= length) ? s : s.slice(0, length) + "…"
|
||||
return s.length <= length ? s : s.slice(0, length) + "…"
|
||||
}
|
||||
|
||||
export function getSearchEngineName(engine: SearchEngines) {
|
||||
switch (engine) {
|
||||
case SearchEngines.Google:
|
||||
case SearchEngines.Google:
|
||||
return intl.get("searchEngine.google")
|
||||
case SearchEngines.Bing:
|
||||
case SearchEngines.Bing:
|
||||
return intl.get("searchEngine.bing")
|
||||
case SearchEngines.Baidu:
|
||||
case SearchEngines.Baidu:
|
||||
return intl.get("searchEngine.baidu")
|
||||
case SearchEngines.DuckDuckGo:
|
||||
case SearchEngines.DuckDuckGo:
|
||||
return intl.get("searchEngine.duckduckgo")
|
||||
}
|
||||
}
|
||||
export function webSearch(text: string, engine=SearchEngines.Google) {
|
||||
export function webSearch(text: string, engine = SearchEngines.Google) {
|
||||
switch (engine) {
|
||||
case SearchEngines.Google:
|
||||
return window.utils.openExternal("https://www.google.com/search?q=" + encodeURIComponent(text))
|
||||
return window.utils.openExternal(
|
||||
"https://www.google.com/search?q=" + encodeURIComponent(text)
|
||||
)
|
||||
case SearchEngines.Bing:
|
||||
return window.utils.openExternal("https://www.bing.com/search?q=" + encodeURIComponent(text))
|
||||
return window.utils.openExternal(
|
||||
"https://www.bing.com/search?q=" + encodeURIComponent(text)
|
||||
)
|
||||
case SearchEngines.Baidu:
|
||||
return window.utils.openExternal("https://www.baidu.com/s?wd=" + encodeURIComponent(text))
|
||||
return window.utils.openExternal(
|
||||
"https://www.baidu.com/s?wd=" + encodeURIComponent(text)
|
||||
)
|
||||
case SearchEngines.DuckDuckGo:
|
||||
return window.utils.openExternal("https://duckduckgo.com/?q=" + encodeURIComponent(text))
|
||||
return window.utils.openExternal(
|
||||
"https://duckduckgo.com/?q=" + encodeURIComponent(text)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function mergeSortedArrays<T>(a: T[], b: T[], cmp: ((x: T, y: T) => number)): T[] {
|
||||
export function mergeSortedArrays<T>(
|
||||
a: T[],
|
||||
b: T[],
|
||||
cmp: (x: T, y: T) => number
|
||||
): T[] {
|
||||
let merged = new Array<T>()
|
||||
let i = 0
|
||||
let j = 0
|
||||
|
@ -177,14 +217,14 @@ export function byteToMB(B: number) {
|
|||
}
|
||||
|
||||
function byteLength(str: string) {
|
||||
var s = str.length;
|
||||
var s = str.length
|
||||
for (var i = str.length - 1; i >= 0; i--) {
|
||||
var code = str.charCodeAt(i);
|
||||
if (code > 0x7f && code <= 0x7ff) s++;
|
||||
else if (code > 0x7ff && code <= 0xffff) s += 2;
|
||||
if (code >= 0xDC00 && code <= 0xDFFF) i--; //trail surrogate
|
||||
var code = str.charCodeAt(i)
|
||||
if (code > 0x7f && code <= 0x7ff) s++
|
||||
else if (code > 0x7ff && code <= 0xffff) s += 2
|
||||
if (code >= 0xdc00 && code <= 0xdfff) i-- //trail surrogate
|
||||
}
|
||||
return s;
|
||||
return s
|
||||
}
|
||||
|
||||
export function calculateItemSize(): Promise<number> {
|
||||
|
@ -218,7 +258,9 @@ export function validateRegex(regex: string, flags = ""): RegExp {
|
|||
}
|
||||
}
|
||||
|
||||
export function platformCtrl(e: React.MouseEvent | React.KeyboardEvent | MouseEvent | KeyboardEvent) {
|
||||
export function platformCtrl(
|
||||
e: React.MouseEvent | React.KeyboardEvent | MouseEvent | KeyboardEvent
|
||||
) {
|
||||
return window.utils.platform === "darwin" ? e.metaKey : e.ctrlKey
|
||||
}
|
||||
|
||||
|
@ -228,6 +270,6 @@ export function initTouchBarWithTexts() {
|
|||
search: intl.get("search"),
|
||||
refresh: intl.get("nav.refresh"),
|
||||
markAll: intl.get("nav.markAllRead"),
|
||||
notifications: intl.get("nav.notifications")
|
||||
notifications: intl.get("nav.notifications"),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,79 +1,81 @@
|
|||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin")
|
||||
const HardSourceWebpackPlugin = require("hard-source-webpack-plugin")
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
mode: 'production',
|
||||
entry: './src/electron.ts',
|
||||
target: 'electron-main',
|
||||
module: {
|
||||
rules: [{
|
||||
test: /\.ts$/,
|
||||
include: /src/,
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js']
|
||||
{
|
||||
mode: "production",
|
||||
entry: "./src/electron.ts",
|
||||
target: "electron-main",
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
include: /src/,
|
||||
resolve: {
|
||||
extensions: [".ts", ".js"],
|
||||
},
|
||||
use: [{ loader: "ts-loader" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
use: [{ loader: 'ts-loader' }]
|
||||
}]
|
||||
},
|
||||
output: {
|
||||
devtoolModuleFilenameTemplate: '[absolute-resource-path]',
|
||||
path: __dirname + '/dist',
|
||||
filename: 'electron.js'
|
||||
},
|
||||
plugins: [
|
||||
new HardSourceWebpackPlugin()
|
||||
]
|
||||
},
|
||||
{
|
||||
mode: 'production',
|
||||
entry: './src/preload.ts',
|
||||
target: 'electron-preload',
|
||||
module: {
|
||||
rules: [{
|
||||
test: /\.ts$/,
|
||||
include: /src/,
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js']
|
||||
output: {
|
||||
devtoolModuleFilenameTemplate: "[absolute-resource-path]",
|
||||
path: __dirname + "/dist",
|
||||
filename: "electron.js",
|
||||
},
|
||||
use: [{ loader: 'ts-loader' }]
|
||||
}]
|
||||
plugins: [new HardSourceWebpackPlugin()],
|
||||
},
|
||||
output: {
|
||||
path: __dirname + '/dist',
|
||||
filename: 'preload.js'
|
||||
},
|
||||
plugins: [
|
||||
new HardSourceWebpackPlugin()
|
||||
]
|
||||
},
|
||||
{
|
||||
mode: 'production',
|
||||
entry: './src/index.tsx',
|
||||
target: 'web',
|
||||
devtool: 'source-map',
|
||||
performance: {
|
||||
hints: false
|
||||
},
|
||||
module: {
|
||||
rules: [{
|
||||
test: /\.ts(x?)$/,
|
||||
include: /src/,
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.js']
|
||||
{
|
||||
mode: "production",
|
||||
entry: "./src/preload.ts",
|
||||
target: "electron-preload",
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
include: /src/,
|
||||
resolve: {
|
||||
extensions: [".ts", ".js"],
|
||||
},
|
||||
use: [{ loader: "ts-loader" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
use: [{ loader: 'ts-loader' }]
|
||||
}]
|
||||
output: {
|
||||
path: __dirname + "/dist",
|
||||
filename: "preload.js",
|
||||
},
|
||||
plugins: [new HardSourceWebpackPlugin()],
|
||||
},
|
||||
output: {
|
||||
path: __dirname + '/dist',
|
||||
filename: 'index.js'
|
||||
{
|
||||
mode: "production",
|
||||
entry: "./src/index.tsx",
|
||||
target: "web",
|
||||
devtool: "source-map",
|
||||
performance: {
|
||||
hints: false,
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts(x?)$/,
|
||||
include: /src/,
|
||||
resolve: {
|
||||
extensions: [".ts", ".tsx", ".js"],
|
||||
},
|
||||
use: [{ loader: "ts-loader" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
output: {
|
||||
path: __dirname + "/dist",
|
||||
filename: "index.js",
|
||||
},
|
||||
plugins: [
|
||||
new HardSourceWebpackPlugin(),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/index.html",
|
||||
}),
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new HardSourceWebpackPlugin(),
|
||||
new HtmlWebpackPlugin({
|
||||
template: './src/index.html'
|
||||
})
|
||||
]
|
||||
}
|
||||
];
|
||||
]
|
||||
|
|
Loading…
Reference in New Issue