add code formatter
This commit is contained in:
parent
0c479f8ddb
commit
6289ef4dd3
6
.github/FUNDING.yml
vendored
6
.github/FUNDING.yml
vendored
@ -9,4 +9,8 @@ community_bridge: # Replace with a single Community Bridge project-name e.g., cl
|
|||||||
liberapay: # Replace with a single Liberapay username
|
liberapay: # Replace with a single Liberapay username
|
||||||
issuehunt: # Replace with a single IssueHunt username
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
otechie: # Replace with a single Otechie 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",
|
||||||
|
]
|
||||||
|
58
.github/workflows/release-linux.yml
vendored
58
.github/workflows/release-linux.yml
vendored
@ -1,39 +1,39 @@
|
|||||||
name: CI/CD Release Linux
|
name: CI/CD Release Linux
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types:
|
types:
|
||||||
- published
|
- published
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release-linux:
|
release-linux:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Build and package the app
|
- name: Build and package the app
|
||||||
run: |
|
run: |
|
||||||
sudo npm install --unsafe-perm=true --allow-root
|
sudo npm install --unsafe-perm=true --allow-root
|
||||||
npm run build
|
npm run build
|
||||||
sudo npm run package-linux
|
sudo npm run package-linux
|
||||||
|
|
||||||
- name: Get app version
|
- name: Get app version
|
||||||
id: package-version
|
id: package-version
|
||||||
uses: martinbeentjes/npm-get-version-action@master
|
uses: martinbeentjes/npm-get-version-action@master
|
||||||
|
|
||||||
- name: Get release
|
- name: Get release
|
||||||
id: get_release
|
id: get_release
|
||||||
uses: bruceadams/get-release@v1.2.0
|
uses: bruceadams/get-release@v1.2.0
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Upload AppImage to release assets
|
- name: Upload AppImage to release assets
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ steps.get_release.outputs.upload_url }}
|
upload_url: ${{ steps.get_release.outputs.upload_url }}
|
||||||
asset_path: ./bin/linux/x64/Fluent Reader-${{ steps.package-version.outputs.current-version }}.AppImage
|
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_name: Fluent.Reader.${{ steps.package-version.outputs.current-version }}.AppImage
|
||||||
asset_content_type: application/octet-stream
|
asset_content_type: application/octet-stream
|
||||||
|
132
.github/workflows/release-main.yml
vendored
132
.github/workflows/release-main.yml
vendored
@ -1,77 +1,77 @@
|
|||||||
name: CI/CD Release
|
name: CI/CD Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- "v*"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Build and package the app
|
- name: Build and package the app
|
||||||
run: |
|
run: |
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
npm run package-win-ci
|
npm run package-win-ci
|
||||||
|
|
||||||
- name: Get app version
|
- name: Get app version
|
||||||
id: package-version
|
id: package-version
|
||||||
run: |
|
run: |
|
||||||
PACKAGE_VERSION=$(cat package.json | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[",]//g' | tr -d '[[:space:]]')
|
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
|
echo ::set-output name=current-version::$PACKAGE_VERSION
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
id: create_release
|
id: create_release
|
||||||
uses: actions/create-release@v1
|
uses: actions/create-release@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ github.ref }}
|
tag_name: ${{ github.ref }}
|
||||||
release_name: Fluent Reader v${{ steps.package-version.outputs.current-version }}
|
release_name: Fluent Reader v${{ steps.package-version.outputs.current-version }}
|
||||||
draft: true
|
draft: true
|
||||||
prerelease: false
|
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: Upload x86 exe to release assets
|
- name: Upload x64 exe to release assets
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
asset_path: ./bin/win32/ia32/Fluent Reader Setup ${{ steps.package-version.outputs.current-version }}.exe
|
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 }}.x86.exe
|
asset_name: Fluent.Reader.Setup.${{ steps.package-version.outputs.current-version }}.x64.exe
|
||||||
asset_content_type: application/vnd.microsoft.portable-executable
|
asset_content_type: application/vnd.microsoft.portable-executable
|
||||||
|
|
||||||
- name: Upload x64 zip to release assets
|
- name: Upload x86 exe to release assets
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
asset_path: ./bin/win32/x64/Fluent Reader-${{ steps.package-version.outputs.current-version }}-win.zip
|
asset_path: ./bin/win32/ia32/Fluent Reader Setup ${{ steps.package-version.outputs.current-version }}.exe
|
||||||
asset_name: Fluent.Reader.Unpacked.${{ steps.package-version.outputs.current-version }}.x64.zip
|
asset_name: Fluent.Reader.Setup.${{ steps.package-version.outputs.current-version }}.x86.exe
|
||||||
asset_content_type: application/zip
|
asset_content_type: application/vnd.microsoft.portable-executable
|
||||||
|
|
||||||
- name: Upload x86 zip to release assets
|
- name: Upload x64 zip to release assets
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
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_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 }}.x86.zip
|
asset_name: Fluent.Reader.Unpacked.${{ steps.package-version.outputs.current-version }}.x64.zip
|
||||||
asset_content_type: application/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
|
||||||
|
12
.prettierignore
Normal file
12
.prettierignore
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
node_modules
|
||||||
|
dist/**/*.js
|
||||||
|
dist/**/*.js.map
|
||||||
|
bin/*
|
||||||
|
.DS_Store
|
||||||
|
*.provisionprofile
|
||||||
|
*.lock
|
||||||
|
|
||||||
|
*.html
|
||||||
|
*.md
|
||||||
|
*.json
|
||||||
|
!src/**/*.json
|
5
.prettierrc.yml
Normal file
5
.prettierrc.yml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
tabWidth: 4
|
||||||
|
semi: false
|
||||||
|
jsxBracketSameLine: true
|
||||||
|
arrowParens: "avoid"
|
||||||
|
quoteProps: "consistent"
|
26
dist/article/article.css
vendored
26
dist/article/article.css
vendored
@ -1,7 +1,9 @@
|
|||||||
@import "../styles/scroll.css";
|
@import "../styles/scroll.css";
|
||||||
|
|
||||||
html, body {
|
html,
|
||||||
font-family: "Segoe UI", "Source Han Sans SC Regular", "Microsoft YaHei", sans-serif;
|
body {
|
||||||
|
font-family: "Segoe UI", "Source Han Sans SC Regular", "Microsoft YaHei",
|
||||||
|
sans-serif;
|
||||||
}
|
}
|
||||||
html {
|
html {
|
||||||
overflow: hidden scroll;
|
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;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
a {
|
a {
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
a:hover, a:active {
|
a:hover,
|
||||||
|
a:active {
|
||||||
color: var(--primary-alt);
|
color: var(--primary-alt);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
@ -64,7 +74,7 @@ a:hover, a:active {
|
|||||||
}
|
}
|
||||||
#main > p.date {
|
#main > p.date {
|
||||||
color: var(--gray);
|
color: var(--gray);
|
||||||
font-size: .875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
article {
|
article {
|
||||||
@ -81,7 +91,7 @@ article figure {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
article figure figcaption {
|
article figure figcaption {
|
||||||
font-size: .875rem;
|
font-size: 0.875rem;
|
||||||
color: var(--gray);
|
color: var(--gray);
|
||||||
-webkit-user-modify: read-only;
|
-webkit-user-modify: read-only;
|
||||||
}
|
}
|
||||||
@ -90,11 +100,11 @@ article iframe {
|
|||||||
}
|
}
|
||||||
article code {
|
article code {
|
||||||
font-family: Monaco, Consolas, monospace;
|
font-family: Monaco, Consolas, monospace;
|
||||||
font-size: .875rem;
|
font-size: 0.875rem;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
article blockquote {
|
article blockquote {
|
||||||
border-left: 2px solid var(--gray);
|
border-left: 2px solid var(--gray);
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
padding: 0 40px;
|
padding: 0 40px;
|
||||||
}
|
}
|
||||||
|
46
dist/styles/cards.css
vendored
46
dist/styles/cards.css
vendored
@ -30,7 +30,8 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.read-indicator, .starred-indicator {
|
.read-indicator,
|
||||||
|
.starred-indicator {
|
||||||
display: block;
|
display: block;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
@ -99,14 +100,15 @@
|
|||||||
background-color: var(--white);
|
background-color: var(--white);
|
||||||
box-shadow: #0004 0 5px 20px;
|
box-shadow: #0004 0 5px 20px;
|
||||||
margin: 18px 12px;
|
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;
|
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;
|
box-shadow: #0006 0 5px 40px;
|
||||||
}
|
}
|
||||||
.default-card:active {
|
.default-card:active {
|
||||||
transform: scale(.97);
|
transform: scale(0.97);
|
||||||
box-shadow: #0004 0 5px 20px;
|
box-shadow: #0004 0 5px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,10 +134,14 @@
|
|||||||
height: 144px;
|
height: 144px;
|
||||||
-webkit-user-drag: none;
|
-webkit-user-drag: none;
|
||||||
}
|
}
|
||||||
.default-card img.head, .default-card p, .default-card h3 {
|
.default-card img.head,
|
||||||
transition: transform ease-out .12s;
|
.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 img.head,
|
||||||
.ms-Fabric--isFocusVisible .default-card.transform:focus p,
|
.ms-Fabric--isFocusVisible .default-card.transform:focus p,
|
||||||
.ms-Fabric--isFocusVisible .default-card.transform:focus h3 {
|
.ms-Fabric--isFocusVisible .default-card.transform:focus h3 {
|
||||||
@ -172,11 +178,12 @@
|
|||||||
|
|
||||||
.list-card {
|
.list-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
transition: box-shadow linear .08s;
|
transition: box-shadow linear 0.08s;
|
||||||
border-bottom: 1px solid var(--neutralQuaternaryAlt);
|
border-bottom: 1px solid var(--neutralQuaternaryAlt);
|
||||||
box-shadow: #0000 0 5px 15px;
|
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;
|
box-shadow: #0004 0 5px 15px;
|
||||||
}
|
}
|
||||||
.list-card:active {
|
.list-card:active {
|
||||||
@ -230,7 +237,8 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
border-left: 2px solid var(--primary);
|
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);
|
color: var(--neutralSecondaryAlt);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,20 +247,22 @@
|
|||||||
padding: 24px;
|
padding: 24px;
|
||||||
max-height: 160px;
|
max-height: 160px;
|
||||||
display: flex;
|
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);
|
border-bottom: 1px solid var(--neutralQuaternaryAlt);
|
||||||
box-shadow: #0000 0 5px 20px;
|
box-shadow: #0000 0 5px 20px;
|
||||||
}
|
}
|
||||||
.magazine-card.read {
|
.magazine-card.read {
|
||||||
color: var(--neutralSecondaryAlt);
|
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;
|
box-shadow: #0004 0 5px 20px;
|
||||||
background-color: var(--white);
|
background-color: var(--white);
|
||||||
}
|
}
|
||||||
.magazine-card:active {
|
.magazine-card:active {
|
||||||
box-shadow: #0000 0 5px 20px;
|
box-shadow: #0000 0 5px 20px;
|
||||||
transform: scale(.97);
|
transform: scale(0.97);
|
||||||
background-color: unset;
|
background-color: unset;
|
||||||
}
|
}
|
||||||
.magazine-card div.head {
|
.magazine-card div.head {
|
||||||
@ -279,7 +289,8 @@
|
|||||||
height: 16px;
|
height: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
.magazine-card h3.title, .magazine-card p.snippet {
|
.magazine-card h3.title,
|
||||||
|
.magazine-card p.snippet {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
@ -304,9 +315,10 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 31px;
|
line-height: 31px;
|
||||||
padding: 0 9px;
|
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;
|
box-shadow: #0004 0 0 10px;
|
||||||
background-color: var(--white);
|
background-color: var(--white);
|
||||||
}
|
}
|
||||||
@ -346,4 +358,4 @@
|
|||||||
}
|
}
|
||||||
.compact-card .time {
|
.compact-card .time {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
20
dist/styles/dark.css
vendored
20
dist/styles/dark.css
vendored
@ -2,10 +2,12 @@
|
|||||||
.ms-Button--commandBar.active .ms-Button-icon {
|
.ms-Button--commandBar.active .ms-Button-icon {
|
||||||
color: #c7e0f4;
|
color: #c7e0f4;
|
||||||
}
|
}
|
||||||
.btn-group .btn:hover, .ms-Nav-compositeLink:hover {
|
.btn-group .btn:hover,
|
||||||
|
.ms-Nav-compositeLink:hover {
|
||||||
background-color: #fff1;
|
background-color: #fff1;
|
||||||
}
|
}
|
||||||
.btn-group .btn:active, .ms-Nav-compositeLink:active {
|
.btn-group .btn:active,
|
||||||
|
.ms-Nav-compositeLink:active {
|
||||||
background-color: #fff2;
|
background-color: #fff2;
|
||||||
}
|
}
|
||||||
.settings .loading {
|
.settings .loading {
|
||||||
@ -14,28 +16,32 @@
|
|||||||
.default-card {
|
.default-card {
|
||||||
box-shadow: #0006 0px 5px 20px;
|
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;
|
box-shadow: #0008 0px 5px 40px;
|
||||||
}
|
}
|
||||||
.default-card div.bg {
|
.default-card div.bg {
|
||||||
background-color: #000b;
|
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;
|
box-shadow: #0006 0px 5px 15px;
|
||||||
}
|
}
|
||||||
.list-card:active {
|
.list-card:active {
|
||||||
box-shadow: #0000 0px 5px 15px, inset #0006 0px 0px 15px;
|
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;
|
box-shadow: #0006 0px 5px 20px;
|
||||||
}
|
}
|
||||||
.magazine-card:active {
|
.magazine-card:active {
|
||||||
box-shadow: #0000 0px 5px 20px;
|
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;
|
box-shadow: #0008 0 0 10px;
|
||||||
}
|
}
|
||||||
.compact-card:active {
|
.compact-card:active {
|
||||||
box-shadow: #0000 0 0 10px;
|
box-shadow: #0000 0 0 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
33
dist/styles/feeds.css
vendored
33
dist/styles/feeds.css
vendored
@ -1,13 +1,18 @@
|
|||||||
@keyframes slideUp20 {
|
@keyframes slideUp20 {
|
||||||
0% { transform: translateY(20px); }
|
0% {
|
||||||
100% { transform: translateY(0); }
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.article-wrapper {
|
.article-wrapper {
|
||||||
margin: 32px auto 0;
|
margin: 32px auto 0;
|
||||||
width: 860px;
|
width: 860px;
|
||||||
height: calc(100% - 50px);
|
height: calc(100% - 50px);
|
||||||
background-color: var(--white);
|
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;
|
border-radius: 5px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
animation-name: slideUp20;
|
animation-name: slideUp20;
|
||||||
@ -17,7 +22,7 @@
|
|||||||
}
|
}
|
||||||
.article-container .btn-group {
|
.article-container .btn-group {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(50% - 32px)
|
top: calc(50% - 32px);
|
||||||
}
|
}
|
||||||
.article-container .btn-group.prev {
|
.article-container .btn-group.prev {
|
||||||
left: calc(50% - 486px);
|
left: calc(50% - 486px);
|
||||||
@ -29,7 +34,8 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
.article webview, .article .error-prompt {
|
.article webview,
|
||||||
|
.article .error-prompt {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100% - 36px);
|
height: calc(100% - 36px);
|
||||||
border: none;
|
border: none;
|
||||||
@ -45,7 +51,8 @@
|
|||||||
color: var(--black);
|
color: var(--black);
|
||||||
border-bottom: 1px solid var(--neutralQuaternaryAlt);
|
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;
|
margin: 8px 8px 11px 0;
|
||||||
}
|
}
|
||||||
.article .actions .ms-Spinner {
|
.article .actions .ms-Spinner {
|
||||||
@ -70,7 +77,8 @@
|
|||||||
content: "/";
|
content: "/";
|
||||||
margin: 0 6px;
|
margin: 0 6px;
|
||||||
}
|
}
|
||||||
.side-article-wrapper, .side-logo-wrapper {
|
.side-article-wrapper,
|
||||||
|
.side-logo-wrapper {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding-top: var(--navHeight);
|
padding-top: var(--navHeight);
|
||||||
height: calc(100% - var(--navHeight));
|
height: calc(100% - var(--navHeight));
|
||||||
@ -108,7 +116,8 @@
|
|||||||
.side-article-wrapper .article > .ms-Stack {
|
.side-article-wrapper .article > .ms-Stack {
|
||||||
border-top: 1px solid var(--neutralQuaternaryAlt);
|
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: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -156,7 +165,8 @@
|
|||||||
padding: 16px 0;
|
padding: 16px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.magazine-feed, .compact-feed {
|
.magazine-feed,
|
||||||
|
.compact-feed {
|
||||||
padding-top: 28px;
|
padding-top: 28px;
|
||||||
height: calc(100% - 60px);
|
height: calc(100% - 60px);
|
||||||
overflow: hidden scroll;
|
overflow: hidden scroll;
|
||||||
@ -184,7 +194,8 @@
|
|||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.cards-feed-container > div.load-more-wrapper, .flex-fix {
|
.cards-feed-container > div.load-more-wrapper,
|
||||||
|
.flex-fix {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.cards-feed-container > div.load-more-wrapper {
|
.cards-feed-container > div.load-more-wrapper {
|
||||||
@ -206,4 +217,4 @@
|
|||||||
color: var(--neutralSecondary);
|
color: var(--neutralSecondary);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
52
dist/styles/global.css
vendored
52
dist/styles/global.css
vendored
@ -46,14 +46,17 @@ body.darwin {
|
|||||||
--navHeight: 38px;
|
--navHeight: 38px;
|
||||||
}
|
}
|
||||||
|
|
||||||
html, body {
|
html,
|
||||||
|
body {
|
||||||
background-color: transparent;
|
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%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
body.win32, body.linux {
|
body.win32,
|
||||||
|
body.linux {
|
||||||
background-color: var(--neutralLighterAlt);
|
background-color: var(--neutralLighterAlt);
|
||||||
}
|
}
|
||||||
#root {
|
#root {
|
||||||
@ -63,12 +66,15 @@ body.win32, body.linux {
|
|||||||
.ms-Link {
|
.ms-Link {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
.ms-ContextualMenu-link, .ms-Button, .ms-ContextualMenu-item button {
|
.ms-ContextualMenu-link,
|
||||||
|
.ms-Button,
|
||||||
|
.ms-ContextualMenu-item button {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
.ms-Nav-link, .ms-Nav-chevronButton {
|
.ms-Nav-link,
|
||||||
|
.ms-Nav-chevronButton {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
@ -105,13 +111,16 @@ i.ms-Nav-chevron {
|
|||||||
.ms-Nav-groupContent {
|
.ms-Nav-groupContent {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
.ms-ActivityItem-activityTypeIcon, .ms-ActivityItem-timeStamp {
|
.ms-ActivityItem-activityTypeIcon,
|
||||||
|
.ms-ActivityItem-timeStamp {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
.ms-Label, .ms-Spinner-label {
|
.ms-Label,
|
||||||
|
.ms-Spinner-label {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
.ms-ActivityItem, .ms-ActivityItem-commentText {
|
.ms-ActivityItem,
|
||||||
|
.ms-ActivityItem-commentText {
|
||||||
color: var(--neutralSecondary);
|
color: var(--neutralSecondary);
|
||||||
}
|
}
|
||||||
.ms-ActivityItem-timeStamp {
|
.ms-ActivityItem-timeStamp {
|
||||||
@ -135,7 +144,8 @@ i.ms-Nav-chevron {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
#root > nav .btn, #root > nav span {
|
#root > nav .btn,
|
||||||
|
#root > nav span {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@ -203,7 +213,8 @@ body.darwin .btn-group .seperator {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
#root > nav .btn-group .btn, .menu .btn-group .btn {
|
#root > nav .btn-group .btn,
|
||||||
|
.menu .btn-group .btn {
|
||||||
height: var(--navHeight);
|
height: var(--navHeight);
|
||||||
line-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 {
|
nav.item-on .btn-group .btn.system {
|
||||||
color: var(--whiteConstant);
|
color: var(--whiteConstant);
|
||||||
}
|
}
|
||||||
.btn-group .btn:hover, .ms-Nav-compositeLink:hover {
|
.btn-group .btn:hover,
|
||||||
|
.ms-Nav-compositeLink:hover {
|
||||||
background-color: #0001;
|
background-color: #0001;
|
||||||
}
|
}
|
||||||
.btn-group .btn:active, .ms-Nav-compositeLink:active {
|
.btn-group .btn:active,
|
||||||
|
.ms-Nav-compositeLink:active {
|
||||||
background-color: #0002;
|
background-color: #0002;
|
||||||
}
|
}
|
||||||
.ms-Nav-compositeLink:hover .ms-Nav-link {
|
.ms-Nav-compositeLink:hover .ms-Nav-link {
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
.btn-group .btn.disabled, .btn-group .btn.fetching {
|
.btn-group .btn.disabled,
|
||||||
|
.btn-group .btn.fetching {
|
||||||
background-color: unset !important;
|
background-color: unset !important;
|
||||||
color: var(--neutralSecondaryAlt);
|
color: var(--neutralSecondaryAlt);
|
||||||
}
|
}
|
||||||
@ -240,8 +254,12 @@ nav.item-on .btn-group .btn.system {
|
|||||||
animation: rotating linear 1.5s infinite;
|
animation: rotating linear 1.5s infinite;
|
||||||
}
|
}
|
||||||
@keyframes rotating {
|
@keyframes rotating {
|
||||||
0% { transform: rotate(0deg); }
|
0% {
|
||||||
100% { transform: rotate(360deg); }
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.btn-group .btn.close:hover {
|
.btn-group .btn.close:hover {
|
||||||
background-color: #e81123;
|
background-color: #e81123;
|
||||||
@ -251,9 +269,9 @@ nav.item-on .btn-group .btn.system {
|
|||||||
background-color: #f1707a;
|
background-color: #f1707a;
|
||||||
color: var(--whiteConstant) !important;
|
color: var(--whiteConstant) !important;
|
||||||
}
|
}
|
||||||
.btn-group .btn.inline-block-wide {
|
.btn-group .btn.inline-block-wide {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
body.darwin .btn-group .btn.system {
|
body.darwin .btn-group .btn.system {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
43
dist/styles/main.css
vendored
43
dist/styles/main.css
vendored
@ -6,10 +6,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fade {
|
@keyframes fade {
|
||||||
0% { opacity: 0; }
|
0% {
|
||||||
100% { opacity: 1; }
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.menu-container, .article-container {
|
.menu-container,
|
||||||
|
.article-container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
left: 0;
|
left: 0;
|
||||||
@ -22,7 +27,9 @@
|
|||||||
animation-name: fade;
|
animation-name: fade;
|
||||||
background-color: #0008;
|
background-color: #0008;
|
||||||
}
|
}
|
||||||
.menu-container, .article-container, .article-wrapper {
|
.menu-container,
|
||||||
|
.article-container,
|
||||||
|
.article-wrapper {
|
||||||
animation-duration: 0.5s;
|
animation-duration: 0.5s;
|
||||||
animation-timing-function: var(--transition-timing);
|
animation-timing-function: var(--transition-timing);
|
||||||
animation-fill-mode: both;
|
animation-fill-mode: both;
|
||||||
@ -45,7 +52,8 @@
|
|||||||
background-color: var(--neutralLighterAltOpacity);
|
background-color: var(--neutralLighterAltOpacity);
|
||||||
backdrop-filter: var(--blur);
|
backdrop-filter: var(--blur);
|
||||||
box-shadow: 5px 0 25px #0004;
|
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);
|
clip-path: inset(0 100% 0 0);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
@ -102,7 +110,8 @@ body.darwin .menu .btn-group {
|
|||||||
width: 680px;
|
width: 680px;
|
||||||
height: calc(100% - 64px);
|
height: calc(100% - 64px);
|
||||||
background-color: var(--white);
|
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;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
div[role="toolbar"] {
|
div[role="toolbar"] {
|
||||||
@ -212,7 +221,7 @@ img.favicon.dropdown {
|
|||||||
.article-search {
|
.article-search {
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top:0;
|
top: 0;
|
||||||
left: 36px;
|
left: 36px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: calc(100% - 484px);
|
max-width: calc(100% - 484px);
|
||||||
@ -220,7 +229,8 @@ img.favicon.dropdown {
|
|||||||
border: none;
|
border: none;
|
||||||
-webkit-app-region: none;
|
-webkit-app-region: none;
|
||||||
height: calc(var(--navHeight) - 4px);
|
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 {
|
body.darwin.not-fullscreen .article-search {
|
||||||
left: 108px;
|
left: 108px;
|
||||||
@ -236,8 +246,9 @@ body.darwin .list-main .article-search {
|
|||||||
top: var(--navHeight);
|
top: var(--navHeight);
|
||||||
margin: 0 10px;
|
margin: 0 10px;
|
||||||
}
|
}
|
||||||
.main, .list-main {
|
.main,
|
||||||
transition: margin-left var(--transition-timing) .367s;
|
.list-main {
|
||||||
|
transition: margin-left var(--transition-timing) 0.367s;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -245,7 +256,8 @@ body.darwin .list-main .article-search {
|
|||||||
#root > nav.menu-on {
|
#root > nav.menu-on {
|
||||||
padding-left: 296px;
|
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;
|
max-width: 300px;
|
||||||
}
|
}
|
||||||
nav.menu-on .btn-group .btn {
|
nav.menu-on .btn-group .btn {
|
||||||
@ -283,7 +295,8 @@ body.darwin .list-main .article-search {
|
|||||||
height: 120%;
|
height: 120%;
|
||||||
box-shadow: inset 5px 0 25px #0004;
|
box-shadow: inset 5px 0 25px #0004;
|
||||||
}
|
}
|
||||||
.main.menu-on, .list-main.menu-on {
|
.main.menu-on,
|
||||||
|
.list-main.menu-on {
|
||||||
margin-left: 280px;
|
margin-left: 280px;
|
||||||
}
|
}
|
||||||
.menu-on .article-search {
|
.menu-on .article-search {
|
||||||
@ -303,10 +316,12 @@ body.darwin .list-main .article-search {
|
|||||||
top: 4px;
|
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;
|
display: none;
|
||||||
}
|
}
|
||||||
.btn-group .btn.inline-block-wide {
|
.btn-group .btn.inline-block-wide {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
html, body {
|
html,
|
||||||
|
body {
|
||||||
background-color: #f3f2f1;
|
background-color: #f3f2f1;
|
||||||
font-family: "Segoe UI", "Microsoft YaHei", sans-serif;
|
font-family: "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -13,13 +14,15 @@ a {
|
|||||||
color: #0078d4;
|
color: #0078d4;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
a:hover, a:active {
|
a:hover,
|
||||||
|
a:active {
|
||||||
color: #004578;
|
color: #004578;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.elevate {
|
.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 {
|
.logo-container {
|
||||||
@ -67,14 +70,16 @@ a:hover, a:active {
|
|||||||
position: relative;
|
position: relative;
|
||||||
top: -280px;
|
top: -280px;
|
||||||
}
|
}
|
||||||
.light-container h1, .dark-container h1 {
|
.light-container h1,
|
||||||
|
.dark-container h1 {
|
||||||
width: 95%;
|
width: 95%;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
margin: 48px auto 24px;
|
margin: 48px auto 24px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.light-container p, .dark-container p {
|
.light-container p,
|
||||||
|
.dark-container p {
|
||||||
width: 85%;
|
width: 85%;
|
||||||
max-width: 750px;
|
max-width: 750px;
|
||||||
margin: 24px auto;
|
margin: 24px auto;
|
||||||
@ -108,7 +113,7 @@ a:hover, a:active {
|
|||||||
.features-container > section > h3 {
|
.features-container > section > h3 {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #605e5c;
|
color: #605e5c;
|
||||||
margin: 0 0 .5em;
|
margin: 0 0 0.5em;
|
||||||
}
|
}
|
||||||
.features-container > section > h3 > span {
|
.features-container > section > h3 > span {
|
||||||
color: #d2d0ce;
|
color: #d2d0ce;
|
||||||
@ -170,13 +175,16 @@ a:hover, a:active {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: calc(50vh - 210px) 0 48px;
|
margin: calc(50vh - 210px) 0 48px;
|
||||||
}
|
}
|
||||||
.links > a {
|
.links > a {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0 8px;
|
margin: 0 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 780px) {
|
@media (max-width: 780px) {
|
||||||
html, body { font-size: 14px; }
|
html,
|
||||||
|
body {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
.logo-container img {
|
.logo-container img {
|
||||||
height: 140px;
|
height: 140px;
|
||||||
width: 140px;
|
width: 140px;
|
||||||
|
@ -3,33 +3,33 @@ buildVersion: 24
|
|||||||
productName: Fluent Reader
|
productName: Fluent Reader
|
||||||
copyright: Copyright © 2020 Haoyuan Liu
|
copyright: Copyright © 2020 Haoyuan Liu
|
||||||
files:
|
files:
|
||||||
- "./dist/**/*"
|
- "./dist/**/*"
|
||||||
- "!**/*.js.map"
|
- "!**/*.js.map"
|
||||||
directories:
|
directories:
|
||||||
output: "./bin/${platform}/${arch}/"
|
output: "./bin/${platform}/${arch}/"
|
||||||
mac:
|
mac:
|
||||||
darkModeSupport: true
|
darkModeSupport: true
|
||||||
target:
|
target:
|
||||||
- dmg
|
- dmg
|
||||||
category: public.app-category.news
|
category: public.app-category.news
|
||||||
electronLanguages:
|
electronLanguages:
|
||||||
- zh_CN
|
- zh_CN
|
||||||
- zh_TW
|
- zh_TW
|
||||||
- en
|
- en
|
||||||
- fr
|
- fr
|
||||||
- es
|
- es
|
||||||
- de
|
- de
|
||||||
- tr
|
- tr
|
||||||
- ja
|
- ja
|
||||||
- sv
|
- sv
|
||||||
- uk
|
- uk
|
||||||
- it
|
- it
|
||||||
- nl
|
- nl
|
||||||
minimumSystemVersion: 10.14.0
|
minimumSystemVersion: 10.14.0
|
||||||
mas:
|
mas:
|
||||||
entitlements: build/entitlements.mas.plist
|
entitlements: build/entitlements.mas.plist
|
||||||
entitlementsInherit: build/entitlements.mas.inherit.plist
|
entitlementsInherit: build/entitlements.mas.inherit.plist
|
||||||
provisioningProfile: build/embedded.provisionprofile
|
provisioningProfile: build/embedded.provisionprofile
|
||||||
hardenedRuntime: false
|
hardenedRuntime: false
|
||||||
gatekeeperAssess: false
|
gatekeeperAssess: false
|
||||||
asarUnpack: []
|
asarUnpack: []
|
||||||
|
@ -2,61 +2,61 @@ appId: me.hyliu.fluentreader
|
|||||||
productName: Fluent Reader
|
productName: Fluent Reader
|
||||||
copyright: Copyright © 2020 Haoyuan Liu
|
copyright: Copyright © 2020 Haoyuan Liu
|
||||||
files:
|
files:
|
||||||
- "./dist/**/*"
|
- "./dist/**/*"
|
||||||
- "!**/*.js.map"
|
- "!**/*.js.map"
|
||||||
directories:
|
directories:
|
||||||
output: "./bin/${platform}/${arch}/"
|
output: "./bin/${platform}/${arch}/"
|
||||||
mac:
|
mac:
|
||||||
darkModeSupport: true
|
darkModeSupport: true
|
||||||
target:
|
target:
|
||||||
- dmg
|
- dmg
|
||||||
category: public.app-category.news
|
category: public.app-category.news
|
||||||
electronLanguages:
|
electronLanguages:
|
||||||
- zh_CN
|
- zh_CN
|
||||||
- zh_TW
|
- zh_TW
|
||||||
- en
|
- en
|
||||||
- fr
|
- fr
|
||||||
- es
|
- es
|
||||||
- de
|
- de
|
||||||
- tr
|
- tr
|
||||||
- ja
|
- ja
|
||||||
- sv
|
- sv
|
||||||
- uk
|
- uk
|
||||||
- it
|
- it
|
||||||
- nl
|
- nl
|
||||||
win:
|
win:
|
||||||
target:
|
target:
|
||||||
- nsis
|
- nsis
|
||||||
- zip
|
- zip
|
||||||
appx:
|
appx:
|
||||||
applicationId: FluentReader
|
applicationId: FluentReader
|
||||||
identityName: 25286HaoyuanLiu.FluentReader
|
identityName: 25286HaoyuanLiu.FluentReader
|
||||||
publisher: CN=FD70E7FA-E5AC-41C4-B9C4-6E8708A6616A
|
publisher: CN=FD70E7FA-E5AC-41C4-B9C4-6E8708A6616A
|
||||||
backgroundColor: transparent
|
backgroundColor: transparent
|
||||||
languages:
|
languages:
|
||||||
- zh-CN
|
- zh-CN
|
||||||
- zh-TW
|
- zh-TW
|
||||||
- en-US
|
- en-US
|
||||||
- fr-FR
|
- fr-FR
|
||||||
- es
|
- es
|
||||||
- de
|
- de
|
||||||
- tr
|
- tr
|
||||||
- ja
|
- ja
|
||||||
- sv
|
- sv
|
||||||
- uk
|
- uk
|
||||||
- it
|
- it
|
||||||
- nl
|
- nl
|
||||||
showNameOnTiles: true
|
showNameOnTiles: true
|
||||||
setBuildNumber: true
|
setBuildNumber: true
|
||||||
nsis:
|
nsis:
|
||||||
oneClick: false
|
oneClick: false
|
||||||
perMachine: true
|
perMachine: true
|
||||||
allowToChangeInstallationDirectory: true
|
allowToChangeInstallationDirectory: true
|
||||||
deleteAppDataOnUninstall: true
|
deleteAppDataOnUninstall: true
|
||||||
linux:
|
linux:
|
||||||
target:
|
target:
|
||||||
- AppImage
|
- AppImage
|
||||||
icon: build/icons
|
icon: build/icons
|
||||||
category: Utility
|
category: Utility
|
||||||
desktop:
|
desktop:
|
||||||
StartupWMClass: fluent-reader
|
StartupWMClass: fluent-reader
|
||||||
|
735
package-lock.json
generated
735
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -7,6 +7,7 @@
|
|||||||
"build": "webpack --config ./webpack.config.js",
|
"build": "webpack --config ./webpack.config.js",
|
||||||
"electron": "electron ./dist/electron.js",
|
"electron": "electron ./dist/electron.js",
|
||||||
"start": "npm run build && npm run electron",
|
"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": "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-win-ci": "electron-builder -w --x64 -p never && electron-builder -w --ia32 -p never",
|
||||||
"package-mac": "electron-builder --mac --x64",
|
"package-mac": "electron-builder --mac --x64",
|
||||||
@ -35,6 +36,7 @@
|
|||||||
"js-md5": "^0.7.3",
|
"js-md5": "^0.7.3",
|
||||||
"lovefield": "^2.1.12",
|
"lovefield": "^2.1.12",
|
||||||
"nedb": "^1.8.0",
|
"nedb": "^1.8.0",
|
||||||
|
"prettier": "2.3.2",
|
||||||
"qrcode.react": "^1.0.0",
|
"qrcode.react": "^1.0.0",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
@ -45,7 +47,7 @@
|
|||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
"reselect": "^4.0.0",
|
"reselect": "^4.0.0",
|
||||||
"ts-loader": "^7.0.4",
|
"ts-loader": "^7.0.4",
|
||||||
"typescript": "^3.9.2",
|
"typescript": "^4.3.5",
|
||||||
"webpack": "^4.43.0",
|
"webpack": "^4.43.0",
|
||||||
"webpack-cli": "^3.3.11"
|
"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"
|
import { ipcRenderer } from "electron"
|
||||||
|
|
||||||
const settingsBridge = {
|
const settingsBridge = {
|
||||||
@ -114,15 +121,15 @@ const settingsBridge = {
|
|||||||
return ipcRenderer.sendSync("get-all-settings") as Object
|
return ipcRenderer.sendSync("get-all-settings") as Object
|
||||||
},
|
},
|
||||||
|
|
||||||
setAll: (configs) => {
|
setAll: configs => {
|
||||||
ipcRenderer.invoke("import-all-settings", configs)
|
ipcRenderer.invoke("import-all-settings", configs)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
settings: typeof settingsBridge
|
settings: typeof settingsBridge
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default settingsBridge
|
export default settingsBridge
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import { ipcRenderer } from "electron"
|
import { ipcRenderer } from "electron"
|
||||||
import { ImageCallbackTypes, TouchBarTexts, WindowStateListenerType } from "../schema-types"
|
import {
|
||||||
|
ImageCallbackTypes,
|
||||||
|
TouchBarTexts,
|
||||||
|
WindowStateListenerType,
|
||||||
|
} from "../schema-types"
|
||||||
import { IObjectWithKey } from "@fluentui/react"
|
import { IObjectWithKey } from "@fluentui/react"
|
||||||
|
|
||||||
const utilsBridge = {
|
const utilsBridge = {
|
||||||
@ -9,7 +13,7 @@ const utilsBridge = {
|
|||||||
return ipcRenderer.sendSync("get-version")
|
return ipcRenderer.sendSync("get-version")
|
||||||
},
|
},
|
||||||
|
|
||||||
openExternal: (url: string, background=false) => {
|
openExternal: (url: string, background = false) => {
|
||||||
ipcRenderer.invoke("open-external", url, background)
|
ipcRenderer.invoke("open-external", url, background)
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -17,12 +21,31 @@ const utilsBridge = {
|
|||||||
ipcRenderer.invoke("show-error-box", title, content)
|
ipcRenderer.invoke("show-error-box", title, content)
|
||||||
},
|
},
|
||||||
|
|
||||||
showMessageBox: async (title: string, message: string, confirm: string, cancel: string, defaultCancel=false, type="none") => {
|
showMessageBox: async (
|
||||||
return await ipcRenderer.invoke("show-message-box", title, message, confirm, cancel, defaultCancel, type) as boolean
|
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) => {
|
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) {
|
if (result) {
|
||||||
return (result: string, errmsg: string) => {
|
return (result: string, errmsg: string) => {
|
||||||
ipcRenderer.invoke("write-save-result", result, errmsg)
|
ipcRenderer.invoke("write-save-result", result, errmsg)
|
||||||
@ -33,7 +56,7 @@ const utilsBridge = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
showOpenDialog: async (filters: Electron.FileFilter[]) => {
|
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> => {
|
getCacheSize: async (): Promise<number> => {
|
||||||
@ -44,13 +67,17 @@ const utilsBridge = {
|
|||||||
await ipcRenderer.invoke("clear-cache")
|
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.removeAllListeners("window-context-menu")
|
||||||
ipcRenderer.on("window-context-menu", (_, pos, text) => {
|
ipcRenderer.on("window-context-menu", (_, pos, text) => {
|
||||||
callback(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.removeAllListeners("webview-context-menu")
|
||||||
ipcRenderer.on("webview-context-menu", (_, pos, text, url) => {
|
ipcRenderer.on("webview-context-menu", (_, pos, text, url) => {
|
||||||
callback(pos, text, url)
|
callback(pos, text, url)
|
||||||
@ -102,7 +129,9 @@ const utilsBridge = {
|
|||||||
requestAttention: () => {
|
requestAttention: () => {
|
||||||
ipcRenderer.invoke("request-attention")
|
ipcRenderer.invoke("request-attention")
|
||||||
},
|
},
|
||||||
addWindowStateListener: (callback: (type: WindowStateListenerType, state: boolean) => any) => {
|
addWindowStateListener: (
|
||||||
|
callback: (type: WindowStateListenerType, state: boolean) => any
|
||||||
|
) => {
|
||||||
ipcRenderer.removeAllListeners("maximized")
|
ipcRenderer.removeAllListeners("maximized")
|
||||||
ipcRenderer.on("maximized", () => {
|
ipcRenderer.on("maximized", () => {
|
||||||
callback(WindowStateListenerType.Maximized, true)
|
callback(WindowStateListenerType.Maximized, true)
|
||||||
@ -132,7 +161,7 @@ const utilsBridge = {
|
|||||||
addTouchBarEventsListener: (callback: (IObjectWithKey) => any) => {
|
addTouchBarEventsListener: (callback: (IObjectWithKey) => any) => {
|
||||||
ipcRenderer.removeAllListeners("touchbar-event")
|
ipcRenderer.removeAllListeners("touchbar-event")
|
||||||
ipcRenderer.on("touchbar-event", (_, key: string) => {
|
ipcRenderer.on("touchbar-event", (_, key: string) => {
|
||||||
callback({ key: key } )
|
callback({ key: key })
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
initTouchBar: (texts: TouchBarTexts) => {
|
initTouchBar: (texts: TouchBarTexts) => {
|
||||||
@ -143,10 +172,10 @@ const utilsBridge = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
utils: typeof utilsBridge
|
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 intl from "react-intl-universal"
|
||||||
import { renderToString } from "react-dom/server"
|
import { renderToString } from "react-dom/server"
|
||||||
import { RSSItem } from "../scripts/models/item"
|
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 { RSSSource, SourceOpenTarget } from "../scripts/models/source"
|
||||||
import { shareSubmenu } from "./context-menu"
|
import { shareSubmenu } from "./context-menu"
|
||||||
import { platformCtrl, decodeFetchResponse } from "../scripts/utils"
|
import { platformCtrl, decodeFetchResponse } from "../scripts/utils"
|
||||||
@ -51,7 +60,8 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
|||||||
window.utils.addWebviewContextListener(this.contextMenuHandler)
|
window.utils.addWebviewContextListener(this.contextMenuHandler)
|
||||||
window.utils.addWebviewKeydownListener(this.keyDownHandler)
|
window.utils.addWebviewKeydownListener(this.keyDownHandler)
|
||||||
window.utils.addWebviewErrorListener(this.webviewError)
|
window.utils.addWebviewErrorListener(this.webviewError)
|
||||||
if (props.source.openTarget === SourceOpenTarget.FullContent) this.loadFull()
|
if (props.source.openTarget === SourceOpenTarget.FullContent)
|
||||||
|
this.loadFull()
|
||||||
}
|
}
|
||||||
|
|
||||||
getFontSize = () => {
|
getFontSize = () => {
|
||||||
@ -59,7 +69,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
|||||||
}
|
}
|
||||||
setFontSize = (size: number) => {
|
setFontSize = (size: number) => {
|
||||||
window.settings.setFontSize(size)
|
window.settings.setFontSize(size)
|
||||||
this.setState({fontSize: size})
|
this.setState({ fontSize: size })
|
||||||
}
|
}
|
||||||
|
|
||||||
fontMenuProps = (): IContextualMenuProps => ({
|
fontMenuProps = (): IContextualMenuProps => ({
|
||||||
@ -68,8 +78,8 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
|||||||
text: String(size),
|
text: String(size),
|
||||||
canCheck: true,
|
canCheck: true,
|
||||||
checked: size === this.state.fontSize,
|
checked: size === this.state.fontSize,
|
||||||
onClick: () => this.setFontSize(size)
|
onClick: () => this.setFontSize(size),
|
||||||
}))
|
})),
|
||||||
})
|
})
|
||||||
|
|
||||||
moreMenuProps = (): IContextualMenuProps => ({
|
moreMenuProps = (): IContextualMenuProps => ({
|
||||||
@ -78,33 +88,46 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
|||||||
key: "openInBrowser",
|
key: "openInBrowser",
|
||||||
text: intl.get("openExternal"),
|
text: intl.get("openExternal"),
|
||||||
iconProps: { iconName: "NavigateExternalInline" },
|
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",
|
key: "copyURL",
|
||||||
text: intl.get("context.copyURL"),
|
text: intl.get("context.copyURL"),
|
||||||
iconProps: { iconName: "Link" },
|
iconProps: { iconName: "Link" },
|
||||||
onClick: () => { window.utils.writeClipboard(this.props.item.link) }
|
onClick: () => {
|
||||||
|
window.utils.writeClipboard(this.props.item.link)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "toggleHidden",
|
key: "toggleHidden",
|
||||||
text: this.props.item.hidden ? intl.get("article.unhide") : intl.get("article.hide"),
|
text: this.props.item.hidden
|
||||||
iconProps: { iconName: this.props.item.hidden ? "View" : "Hide3" },
|
? intl.get("article.unhide")
|
||||||
onClick: () => { this.props.toggleHidden(this.props.item) }
|
: intl.get("article.hide"),
|
||||||
|
iconProps: {
|
||||||
|
iconName: this.props.item.hidden ? "View" : "Hide3",
|
||||||
|
},
|
||||||
|
onClick: () => {
|
||||||
|
this.props.toggleHidden(this.props.item)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "fontMenu",
|
key: "fontMenu",
|
||||||
text: intl.get("article.fontSize"),
|
text: intl.get("article.fontSize"),
|
||||||
iconProps: { iconName: "FontSize" },
|
iconProps: { iconName: "FontSize" },
|
||||||
disabled: this.state.loadWebpage,
|
disabled: this.state.loadWebpage,
|
||||||
subMenuProps: this.fontMenuProps()
|
subMenuProps: this.fontMenuProps(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "divider_1",
|
key: "divider_1",
|
||||||
itemType: ContextualMenuItemType.Divider,
|
itemType: ContextualMenuItemType.Divider,
|
||||||
},
|
},
|
||||||
...shareSubmenu(this.props.item)
|
...shareSubmenu(this.props.item),
|
||||||
]
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
contextMenuHandler = (pos: [number, number], text: string, url: string) => {
|
contextMenuHandler = (pos: [number, number], text: string, url: string) => {
|
||||||
@ -126,13 +149,16 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
|||||||
case "ArrowRight":
|
case "ArrowRight":
|
||||||
this.props.offsetItem(input.key === "ArrowLeft" ? -1 : 1)
|
this.props.offsetItem(input.key === "ArrowLeft" ? -1 : 1)
|
||||||
break
|
break
|
||||||
case "l": case "L":
|
case "l":
|
||||||
|
case "L":
|
||||||
this.toggleWebpage()
|
this.toggleWebpage()
|
||||||
break
|
break
|
||||||
case "w": case "W":
|
case "w":
|
||||||
|
case "W":
|
||||||
this.toggleFull()
|
this.toggleFull()
|
||||||
break
|
break
|
||||||
case "H": case "h":
|
case "H":
|
||||||
|
case "h":
|
||||||
if (!input.meta) this.props.toggleHidden(this.props.item)
|
if (!input.meta) this.props.toggleHidden(this.props.item)
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
@ -144,7 +170,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
|||||||
ctrlKey: input.control,
|
ctrlKey: input.control,
|
||||||
metaKey: input.meta,
|
metaKey: input.meta,
|
||||||
repeat: input.isAutoRepeat,
|
repeat: input.isAutoRepeat,
|
||||||
bubbles: true
|
bubbles: true,
|
||||||
})
|
})
|
||||||
this.props.shortcuts(this.props.item, keyboardEvent)
|
this.props.shortcuts(this.props.item, keyboardEvent)
|
||||||
document.dispatchEvent(keyboardEvent)
|
document.dispatchEvent(keyboardEvent)
|
||||||
@ -154,14 +180,14 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
webviewLoaded = () => {
|
webviewLoaded = () => {
|
||||||
this.setState({loaded: true})
|
this.setState({ loaded: true })
|
||||||
}
|
}
|
||||||
webviewError = (reason: string) => {
|
webviewError = (reason: string) => {
|
||||||
this.setState({error: true, errorDescription: reason})
|
this.setState({ error: true, errorDescription: reason })
|
||||||
}
|
}
|
||||||
webviewReload = () => {
|
webviewReload = () => {
|
||||||
if (this.webview) {
|
if (this.webview) {
|
||||||
this.setState({loaded: false, error: false})
|
this.setState({ loaded: false, error: false })
|
||||||
this.webview.reload()
|
this.webview.reload()
|
||||||
} else if (this.state.loadFull) {
|
} else if (this.state.loadFull) {
|
||||||
this.loadFull()
|
this.loadFull()
|
||||||
@ -174,9 +200,11 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
|||||||
this.webview = webview
|
this.webview = webview
|
||||||
if (webview) {
|
if (webview) {
|
||||||
webview.focus()
|
webview.focus()
|
||||||
this.setState({loaded: false, error: false})
|
this.setState({ loaded: false, error: false })
|
||||||
webview.addEventListener("did-stop-loading", this.webviewLoaded)
|
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
|
// @ts-ignore
|
||||||
if (card) card.scrollIntoViewIfNeeded()
|
if (card) card.scrollIntoViewIfNeeded()
|
||||||
}
|
}
|
||||||
@ -185,23 +213,32 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
|||||||
componentDidUpdate = (prevProps: ArticleProps) => {
|
componentDidUpdate = (prevProps: ArticleProps) => {
|
||||||
if (prevProps.item._id != this.props.item._id) {
|
if (prevProps.item._id != this.props.item._id) {
|
||||||
this.setState({
|
this.setState({
|
||||||
loadWebpage: this.props.source.openTarget === SourceOpenTarget.Webpage,
|
loadWebpage:
|
||||||
loadFull: this.props.source.openTarget === SourceOpenTarget.FullContent,
|
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()
|
this.componentDidMount()
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount = () => {
|
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()
|
if (refocus) refocus.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleWebpage = () => {
|
toggleWebpage = () => {
|
||||||
if (this.state.loadWebpage) {
|
if (this.state.loadWebpage) {
|
||||||
this.setState({ loadWebpage: false })
|
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 })
|
this.setState({ loadWebpage: true, loadFull: false })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -209,7 +246,10 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
|||||||
toggleFull = () => {
|
toggleFull = () => {
|
||||||
if (this.state.loadFull) {
|
if (this.state.loadFull) {
|
||||||
this.setState({ loadFull: false })
|
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.setState({ loadFull: true, loadWebpage: false })
|
||||||
this.loadFull()
|
this.loadFull()
|
||||||
}
|
}
|
||||||
@ -222,86 +262,173 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
|||||||
const html = await decodeFetchResponse(result, true)
|
const html = await decodeFetchResponse(result, true)
|
||||||
this.setState({ fullContent: html })
|
this.setState({ fullContent: html })
|
||||||
} catch {
|
} catch {
|
||||||
this.setState({ loaded: true, error: true, errorDescription: "MERCURY_PARSER_FAILURE" })
|
this.setState({
|
||||||
|
loaded: true,
|
||||||
|
error: true,
|
||||||
|
errorDescription: "MERCURY_PARSER_FAILURE",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
articleView = () => {
|
articleView = () => {
|
||||||
const a = encodeURIComponent(this.state.loadFull ? this.state.fullContent : this.props.item.content)
|
const a = encodeURIComponent(
|
||||||
const h = encodeURIComponent(renderToString(<>
|
this.state.loadFull
|
||||||
<p className="title">{this.props.item.title}</p>
|
? this.state.fullContent
|
||||||
<p className="date">{this.props.item.date.toLocaleString(this.props.locale, {hour12: !this.props.locale.startsWith("zh")})}</p>
|
: this.props.item.content
|
||||||
<article></article>
|
)
|
||||||
</>))
|
const h = encodeURIComponent(
|
||||||
return `article/article.html?a=${a}&h=${h}&s=${this.state.fontSize}&u=${this.props.item.link}&m=${this.state.loadFull?1:0}`
|
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 = () => (
|
render = () => (
|
||||||
<FocusZone className="article">
|
<FocusZone className="article">
|
||||||
<Stack horizontal style={{height: 36}}>
|
<Stack horizontal style={{ height: 36 }}>
|
||||||
<span style={{width: 96}}></span>
|
<span style={{ width: 96 }}></span>
|
||||||
<Stack className="actions" grow horizontal tokens={{childrenGap: 12}}>
|
<Stack
|
||||||
|
className="actions"
|
||||||
|
grow
|
||||||
|
horizontal
|
||||||
|
tokens={{ childrenGap: 12 }}>
|
||||||
<Stack.Item grow>
|
<Stack.Item grow>
|
||||||
<span className="source-name">
|
<span className="source-name">
|
||||||
{this.state.loaded
|
{this.state.loaded ? (
|
||||||
? (this.props.source.iconurl && <img className="favicon" src={this.props.source.iconurl} />)
|
this.props.source.iconurl && (
|
||||||
: <Spinner size={1} />}
|
<img
|
||||||
|
className="favicon"
|
||||||
|
src={this.props.source.iconurl}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Spinner size={1} />
|
||||||
|
)}
|
||||||
{this.props.source.name}
|
{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>
|
</span>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<CommandBarButton
|
<CommandBarButton
|
||||||
title={this.props.item.hasRead ? intl.get("article.markUnread") : intl.get("article.markRead")}
|
title={
|
||||||
iconProps={this.props.item.hasRead
|
this.props.item.hasRead
|
||||||
? {iconName: "StatusCircleRing"}
|
? intl.get("article.markUnread")
|
||||||
: {iconName: "RadioBtnOn", style: {fontSize: 14, textAlign: "center"}}}
|
: intl.get("article.markRead")
|
||||||
onClick={() => this.props.toggleHasRead(this.props.item)} />
|
}
|
||||||
|
iconProps={
|
||||||
|
this.props.item.hasRead
|
||||||
|
? { iconName: "StatusCircleRing" }
|
||||||
|
: {
|
||||||
|
iconName: "RadioBtnOn",
|
||||||
|
style: {
|
||||||
|
fontSize: 14,
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onClick={() =>
|
||||||
|
this.props.toggleHasRead(this.props.item)
|
||||||
|
}
|
||||||
|
/>
|
||||||
<CommandBarButton
|
<CommandBarButton
|
||||||
title={this.props.item.starred ? intl.get("article.unstar") : intl.get("article.star")}
|
title={
|
||||||
iconProps={{iconName: this.props.item.starred ? "FavoriteStarFill" : "FavoriteStar"}}
|
this.props.item.starred
|
||||||
onClick={() => this.props.toggleStarred(this.props.item)} />
|
? intl.get("article.unstar")
|
||||||
|
: intl.get("article.star")
|
||||||
|
}
|
||||||
|
iconProps={{
|
||||||
|
iconName: this.props.item.starred
|
||||||
|
? "FavoriteStarFill"
|
||||||
|
: "FavoriteStar",
|
||||||
|
}}
|
||||||
|
onClick={() =>
|
||||||
|
this.props.toggleStarred(this.props.item)
|
||||||
|
}
|
||||||
|
/>
|
||||||
<CommandBarButton
|
<CommandBarButton
|
||||||
title={intl.get("article.loadFull")}
|
title={intl.get("article.loadFull")}
|
||||||
className={this.state.loadFull ? "active" : ""}
|
className={this.state.loadFull ? "active" : ""}
|
||||||
iconProps={{iconName: "RawSource"}}
|
iconProps={{ iconName: "RawSource" }}
|
||||||
onClick={this.toggleFull} />
|
onClick={this.toggleFull}
|
||||||
|
/>
|
||||||
<CommandBarButton
|
<CommandBarButton
|
||||||
title={intl.get("article.loadWebpage")}
|
title={intl.get("article.loadWebpage")}
|
||||||
className={this.state.loadWebpage ? "active" : ""}
|
className={this.state.loadWebpage ? "active" : ""}
|
||||||
iconProps={{iconName: "Globe"}}
|
iconProps={{ iconName: "Globe" }}
|
||||||
onClick={this.toggleWebpage} />
|
onClick={this.toggleWebpage}
|
||||||
|
/>
|
||||||
<CommandBarButton
|
<CommandBarButton
|
||||||
title={intl.get("more")}
|
title={intl.get("more")}
|
||||||
iconProps={{iconName: "More"}}
|
iconProps={{ iconName: "More" }}
|
||||||
menuIconProps={{style: {display: "none"}}}
|
menuIconProps={{ style: { display: "none" } }}
|
||||||
menuProps={this.moreMenuProps()} />
|
menuProps={this.moreMenuProps()}
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack horizontal horizontalAlign="end" style={{width: 112}}>
|
<Stack horizontal horizontalAlign="end" style={{ width: 112 }}>
|
||||||
<CommandBarButton
|
<CommandBarButton
|
||||||
title={intl.get("close")}
|
title={intl.get("close")}
|
||||||
iconProps={{iconName: "BackToWindow"}}
|
iconProps={{ iconName: "BackToWindow" }}
|
||||||
onClick={this.props.dismiss} />
|
onClick={this.props.dismiss}
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
{(!this.state.loadFull || this.state.fullContent) && <webview
|
{(!this.state.loadFull || this.state.fullContent) && (
|
||||||
id="article"
|
<webview
|
||||||
className={this.state.error ? "error" : ""}
|
id="article"
|
||||||
key={this.props.item._id + (this.state.loadWebpage ? "_" : "")}
|
className={this.state.error ? "error" : ""}
|
||||||
src={this.state.loadWebpage ? this.props.item.link : this.articleView()}
|
key={
|
||||||
webpreferences="contextIsolation,disableDialogs,autoplayPolicy=document-user-activation-required"
|
this.props.item._id +
|
||||||
partition={this.state.loadWebpage ? "sandbox" : undefined} />}
|
(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 && (
|
{this.state.error && (
|
||||||
<Stack className="error-prompt" verticalAlign="center" horizontalAlign="center" tokens={{childrenGap: 12}}>
|
<Stack
|
||||||
<Icon iconName="HeartBroken" style={{fontSize: 32}} />
|
className="error-prompt"
|
||||||
<Stack horizontal horizontalAlign="center" tokens={{childrenGap: 7}}>
|
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>{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>
|
</Stack>
|
||||||
<span style={{fontSize: 11}}>{this.state.errorDescription}</span>
|
<span style={{ fontSize: 11 }}>
|
||||||
|
{this.state.errorDescription}
|
||||||
|
</span>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</FocusZone>
|
</FocusZone>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Article
|
export default Article
|
||||||
|
@ -61,4 +61,4 @@ export namespace Card {
|
|||||||
const onKeyDown = (props: Props, e: React.KeyboardEvent) => {
|
const onKeyDown = (props: Props, e: React.KeyboardEvent) => {
|
||||||
props.shortcuts(props.item, e.nativeEvent)
|
props.shortcuts(props.item, e.nativeEvent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ const className = (props: Card.Props) => {
|
|||||||
return cn.join(" ")
|
return cn.join(" ")
|
||||||
}
|
}
|
||||||
|
|
||||||
const CompactCard: React.FunctionComponent<Card.Props> = (props) => (
|
const CompactCard: React.FunctionComponent<Card.Props> = props => (
|
||||||
<div
|
<div
|
||||||
className={className(props)}
|
className={className(props)}
|
||||||
{...Card.bindEventsToProps(props)}
|
{...Card.bindEventsToProps(props)}
|
||||||
@ -18,11 +18,19 @@ const CompactCard: React.FunctionComponent<Card.Props> = (props) => (
|
|||||||
data-is-focusable>
|
data-is-focusable>
|
||||||
<CardInfo source={props.source} item={props.item} hideTime />
|
<CardInfo source={props.source} item={props.item} hideTime />
|
||||||
<div className="data">
|
<div className="data">
|
||||||
<span className="title"><Highlights text={props.item.title} filter={props.filter} title /></span>
|
<span className="title">
|
||||||
<span className="snippet"><Highlights text={props.item.snippet} filter={props.filter} /></span>
|
<Highlights
|
||||||
|
text={props.item.title}
|
||||||
|
filter={props.filter}
|
||||||
|
title
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className="snippet">
|
||||||
|
<Highlights text={props.item.snippet} filter={props.filter} />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Time date={props.item.date} />
|
<Time date={props.item.date} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
export default CompactCard
|
export default CompactCard
|
||||||
|
@ -10,7 +10,7 @@ const className = (props: Card.Props) => {
|
|||||||
return cn.join(" ")
|
return cn.join(" ")
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultCard: React.FunctionComponent<Card.Props> = (props) => (
|
const DefaultCard: React.FunctionComponent<Card.Props> = props => (
|
||||||
<div
|
<div
|
||||||
className={className(props)}
|
className={className(props)}
|
||||||
{...Card.bindEventsToProps(props)}
|
{...Card.bindEventsToProps(props)}
|
||||||
@ -24,11 +24,13 @@ const DefaultCard: React.FunctionComponent<Card.Props> = (props) => (
|
|||||||
<img className="head" src={props.item.thumb} />
|
<img className="head" src={props.item.thumb} />
|
||||||
) : null}
|
) : null}
|
||||||
<CardInfo source={props.source} item={props.item} />
|
<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")}>
|
<p className={"snippet" + (props.item.thumb ? "" : " show")}>
|
||||||
<Highlights text={props.item.snippet} filter={props.filter} />
|
<Highlights text={props.item.snippet} filter={props.filter} />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
export default DefaultCard
|
export default DefaultCard
|
||||||
|
@ -8,11 +8,14 @@ type HighlightsProps = {
|
|||||||
title?: boolean
|
title?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const Highlights: React.FunctionComponent<HighlightsProps> = (props) => {
|
const Highlights: React.FunctionComponent<HighlightsProps> = props => {
|
||||||
const spans: [string, boolean][] = new Array()
|
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
|
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])
|
if (props.title) spans.push([props.text, false])
|
||||||
else spans.push([props.text.substr(0, 325), false])
|
else spans.push([props.text.substr(0, 325), false])
|
||||||
} else if (props.title) {
|
} else if (props.title) {
|
||||||
@ -22,7 +25,10 @@ const Highlights: React.FunctionComponent<HighlightsProps> = (props) => {
|
|||||||
match = regex.exec(props.text)
|
match = regex.exec(props.text)
|
||||||
if (match) {
|
if (match) {
|
||||||
if (startIndex != match.index) {
|
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])
|
spans.push([match[0], true])
|
||||||
} else {
|
} else {
|
||||||
@ -33,8 +39,14 @@ const Highlights: React.FunctionComponent<HighlightsProps> = (props) => {
|
|||||||
const match = regex.exec(props.text)
|
const match = regex.exec(props.text)
|
||||||
if (match) {
|
if (match) {
|
||||||
if (match.index != 0) {
|
if (match.index != 0) {
|
||||||
const startIndex = Math.max(match.index - 25, props.text.lastIndexOf(" ", Math.max(match.index - 10, 0)))
|
const startIndex = Math.max(
|
||||||
spans.push([props.text.substring(Math.max(0, startIndex), match.index), false])
|
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])
|
spans.push([match[0], true])
|
||||||
if (regex.lastIndex < props.text.length) {
|
if (regex.lastIndex < props.text.length) {
|
||||||
@ -45,9 +57,13 @@ const Highlights: React.FunctionComponent<HighlightsProps> = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return (
|
||||||
{spans.map(([text, flag]) => flag ? <span className="h">{text}</span> : text)}
|
<>
|
||||||
</>
|
{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
|
showCreator?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const CardInfo: React.FunctionComponent<CardInfoProps> = (props) => (
|
const CardInfo: React.FunctionComponent<CardInfoProps> = props => (
|
||||||
<p className="info">
|
<p className="info">
|
||||||
{props.source.iconurl ? <img src={props.source.iconurl} /> : null}
|
{props.source.iconurl ? <img src={props.source.iconurl} /> : null}
|
||||||
<span className="name">
|
<span className="name">
|
||||||
@ -19,10 +19,12 @@ const CardInfo: React.FunctionComponent<CardInfoProps> = (props) => (
|
|||||||
<span className="creator">{props.item.creator}</span>
|
<span className="creator">{props.item.creator}</span>
|
||||||
)}
|
)}
|
||||||
</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.item.hasRead ? null : <span className="read-indicator"></span>}
|
||||||
{props.hideTime ? null : <Time date={props.item.date} />}
|
{props.hideTime ? null : <Time date={props.item.date} />}
|
||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
|
|
||||||
export default CardInfo
|
export default CardInfo
|
||||||
|
@ -8,27 +8,41 @@ const className = (props: Card.Props) => {
|
|||||||
let cn = ["card", "list-card"]
|
let cn = ["card", "list-card"]
|
||||||
if (props.item.hidden) cn.push("hidden")
|
if (props.item.hidden) cn.push("hidden")
|
||||||
if (props.selected) cn.push("selected")
|
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(" ")
|
return cn.join(" ")
|
||||||
}
|
}
|
||||||
|
|
||||||
const ListCard: React.FunctionComponent<Card.Props> = (props) => (
|
const ListCard: React.FunctionComponent<Card.Props> = props => (
|
||||||
<div
|
<div
|
||||||
className={className(props)}
|
className={className(props)}
|
||||||
{...Card.bindEventsToProps(props)}
|
{...Card.bindEventsToProps(props)}
|
||||||
data-iid={props.item._id}
|
data-iid={props.item._id}
|
||||||
data-is-focusable>
|
data-is-focusable>
|
||||||
{props.item.thumb && (props.viewConfigs & ViewConfigs.ShowCover) ? (
|
{props.item.thumb && props.viewConfigs & ViewConfigs.ShowCover ? (
|
||||||
<div className="head"><img src={props.item.thumb} /></div>
|
<div className="head">
|
||||||
|
<img src={props.item.thumb} />
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="data">
|
<div className="data">
|
||||||
<CardInfo source={props.source} item={props.item} />
|
<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) && (
|
{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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
export default ListCard
|
export default ListCard
|
||||||
|
@ -10,23 +10,36 @@ const className = (props: Card.Props) => {
|
|||||||
return cn.join(" ")
|
return cn.join(" ")
|
||||||
}
|
}
|
||||||
|
|
||||||
const MagazineCard: React.FunctionComponent<Card.Props> = (props) => (
|
const MagazineCard: React.FunctionComponent<Card.Props> = props => (
|
||||||
<div
|
<div
|
||||||
className={className(props)}
|
className={className(props)}
|
||||||
{...Card.bindEventsToProps(props)}
|
{...Card.bindEventsToProps(props)}
|
||||||
data-iid={props.item._id}
|
data-iid={props.item._id}
|
||||||
data-is-focusable>
|
data-is-focusable>
|
||||||
{props.item.thumb ? (
|
{props.item.thumb ? (
|
||||||
<div className="head"><img src={props.item.thumb} /></div>
|
<div className="head">
|
||||||
|
<img src={props.item.thumb} />
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="data">
|
<div className="data">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="title"><Highlights text={props.item.title} filter={props.filter} title /></h3>
|
<h3 className="title">
|
||||||
<p className="snippet"><Highlights text={props.item.snippet} filter={props.filter} /></p>
|
<Highlights
|
||||||
|
text={props.item.title}
|
||||||
|
filter={props.filter}
|
||||||
|
title
|
||||||
|
/>
|
||||||
|
</h3>
|
||||||
|
<p className="snippet">
|
||||||
|
<Highlights
|
||||||
|
text={props.item.snippet}
|
||||||
|
filter={props.filter}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<CardInfo source={props.source} item={props.item} showCreator />
|
<CardInfo source={props.source} item={props.item} showCreator />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
export default MagazineCard
|
export default MagazineCard
|
||||||
|
@ -1,8 +1,18 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import intl from "react-intl-universal"
|
import intl from "react-intl-universal"
|
||||||
import QRCode from "qrcode.react"
|
import QRCode from "qrcode.react"
|
||||||
import { cutText, webSearch, getSearchEngineName, platformCtrl } from "../scripts/utils"
|
import {
|
||||||
import { ContextualMenu, IContextualMenuItem, ContextualMenuItemType, DirectionalHint } from "office-ui-fabric-react/lib/ContextualMenu"
|
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 { ContextMenuType } from "../scripts/models/app"
|
||||||
import { RSSItem } from "../scripts/models/item"
|
import { RSSItem } from "../scripts/models/item"
|
||||||
import { ContextReduxProps } from "../containers/context-menu-container"
|
import { ContextReduxProps } from "../containers/context-menu-container"
|
||||||
@ -37,15 +47,12 @@ export type ContextMenuProps = ContextReduxProps & {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const shareSubmenu = (item: RSSItem): IContextualMenuItem[] => [
|
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) => (
|
export const renderShareQR = (item: IContextualMenuItem) => (
|
||||||
<div className="qr-container">
|
<div className="qr-container">
|
||||||
<QRCode
|
<QRCode value={item.url} size={150} renderAs="svg" />
|
||||||
value={item.url}
|
|
||||||
size={150}
|
|
||||||
renderAs="svg" />
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -55,143 +62,222 @@ function getSearchItem(text: string): IContextualMenuItem {
|
|||||||
key: "searchText",
|
key: "searchText",
|
||||||
text: intl.get("context.search", {
|
text: intl.get("context.search", {
|
||||||
text: cutText(text, 15),
|
text: cutText(text, 15),
|
||||||
engine: getSearchEngineName(engine)
|
engine: getSearchEngineName(engine),
|
||||||
}),
|
}),
|
||||||
iconProps: { iconName: "Search" },
|
iconProps: { iconName: "Search" },
|
||||||
onClick: () => webSearch(text, engine)
|
onClick: () => webSearch(text, engine),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ContextMenu extends React.Component<ContextMenuProps> {
|
export class ContextMenu extends React.Component<ContextMenuProps> {
|
||||||
getItems = (): IContextualMenuItem[] => {
|
getItems = (): IContextualMenuItem[] => {
|
||||||
switch (this.props.type) {
|
switch (this.props.type) {
|
||||||
case ContextMenuType.Item: return [
|
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 ? [
|
|
||||||
{
|
{
|
||||||
key: "divider_2",
|
key: "showItem",
|
||||||
itemType: ContextualMenuItemType.Divider,
|
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",
|
key: "openInBrowser",
|
||||||
text: intl.get("context.view"),
|
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: {
|
subMenuProps: {
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
key: "showCover",
|
key: "markBelow",
|
||||||
text: intl.get("context.showCover"),
|
text: intl.get("article.markBelow"),
|
||||||
canCheck: true,
|
iconProps: {
|
||||||
checked: Boolean(this.props.viewConfigs & ViewConfigs.ShowCover),
|
iconName: "Down",
|
||||||
onClick: () => this.props.setViewConfigs(this.props.viewConfigs ^ ViewConfigs.ShowCover)
|
style: { fontSize: 14 },
|
||||||
|
},
|
||||||
|
onClick: () =>
|
||||||
|
this.props.markAllRead(
|
||||||
|
null,
|
||||||
|
this.props.item.date
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "showSnippet",
|
key: "markAbove",
|
||||||
text: intl.get("context.showSnippet"),
|
text: intl.get("article.markAbove"),
|
||||||
canCheck: true,
|
iconProps: {
|
||||||
checked: Boolean(this.props.viewConfigs & ViewConfigs.ShowSnippet),
|
iconName: "Up",
|
||||||
onClick: () => this.props.setViewConfigs(this.props.viewConfigs ^ ViewConfigs.ShowSnippet)
|
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",
|
key: "toggleStarred",
|
||||||
text: intl.get("context.copy"),
|
text: this.props.item.starred
|
||||||
iconProps: { iconName: "Copy" },
|
? intl.get("article.unstar")
|
||||||
onClick: () => { window.utils.writeClipboard(this.props.text) }
|
: 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) {
|
if (this.props.url) {
|
||||||
items.push({
|
items.push({
|
||||||
key: "urlSection",
|
key: "urlSection",
|
||||||
@ -202,229 +288,320 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
|
|||||||
{
|
{
|
||||||
key: "openInBrowser",
|
key: "openInBrowser",
|
||||||
text: intl.get("openExternal"),
|
text: intl.get("openExternal"),
|
||||||
iconProps: { iconName: "NavigateExternalInline" },
|
iconProps: {
|
||||||
onClick: (e) => { window.utils.openExternal(this.props.url, platformCtrl(e)) }
|
iconName: "NavigateExternalInline",
|
||||||
|
},
|
||||||
|
onClick: e => {
|
||||||
|
window.utils.openExternal(
|
||||||
|
this.props.url,
|
||||||
|
platformCtrl(e)
|
||||||
|
)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "copyURL",
|
key: "copyURL",
|
||||||
text: intl.get("context.copyURL"),
|
text: intl.get("context.copyURL"),
|
||||||
iconProps: { iconName: "Link" },
|
iconProps: { iconName: "Link" },
|
||||||
onClick: () => { window.utils.writeClipboard(this.props.url) }
|
onClick: () => {
|
||||||
}
|
window.utils.writeClipboard(
|
||||||
]
|
this.props.url
|
||||||
}
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
case ContextMenuType.Image: return [
|
case ContextMenuType.Image:
|
||||||
{
|
return [
|
||||||
key: "openInBrowser",
|
{
|
||||||
text: intl.get("openExternal"),
|
key: "openInBrowser",
|
||||||
iconProps: { iconName: "NavigateExternalInline" },
|
text: intl.get("openExternal"),
|
||||||
onClick: (e) => {
|
iconProps: { iconName: "NavigateExternalInline" },
|
||||||
if (platformCtrl(e)) {
|
onClick: e => {
|
||||||
window.utils.imageCallback(ImageCallbackTypes.OpenExternalBg)
|
if (platformCtrl(e)) {
|
||||||
} else {
|
window.utils.imageCallback(
|
||||||
window.utils.imageCallback(ImageCallbackTypes.OpenExternal)
|
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)
|
|
||||||
}
|
}
|
||||||
]
|
},
|
||||||
}
|
},
|
||||||
},
|
{
|
||||||
{
|
key: "saveImageAs",
|
||||||
key: "section_3",
|
text: intl.get("context.saveImageAs"),
|
||||||
itemType: ContextualMenuItemType.Section,
|
iconProps: { iconName: "SaveTemplate" },
|
||||||
sectionProps: {
|
onClick: () => {
|
||||||
title: intl.get("search"),
|
window.utils.imageCallback(
|
||||||
bottomDivider: true,
|
ImageCallbackTypes.SaveAs
|
||||||
items: [
|
)
|
||||||
{
|
},
|
||||||
key: "caseSensitive",
|
},
|
||||||
text: intl.get("context.caseSensitive"),
|
{
|
||||||
iconProps: { style: { fontSize: 12, fontStyle: "normal" }, children: "Aa" },
|
key: "copyImage",
|
||||||
canCheck: true,
|
text: intl.get("context.copyImage"),
|
||||||
checked: !(this.props.filter & FilterType.CaseInsensitive),
|
iconProps: { iconName: "FileImage" },
|
||||||
onClick: () => this.props.toggleFilter(FilterType.CaseInsensitive)
|
onClick: () => {
|
||||||
},
|
window.utils.imageCallback(ImageCallbackTypes.Copy)
|
||||||
{
|
},
|
||||||
key: "fullSearch",
|
},
|
||||||
text: intl.get("context.fullSearch"),
|
{
|
||||||
iconProps: { iconName: "Breadcrumb" },
|
key: "copyImageURL",
|
||||||
canCheck: true,
|
text: intl.get("context.copyImageURL"),
|
||||||
checked: Boolean(this.props.filter & FilterType.FullSearch),
|
iconProps: { iconName: "Link" },
|
||||||
onClick: () => this.props.toggleFilter(FilterType.FullSearch)
|
onClick: () => {
|
||||||
},
|
window.utils.imageCallback(
|
||||||
]
|
ImageCallbackTypes.CopyLink
|
||||||
}
|
)
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
key: "showHidden",
|
]
|
||||||
text: intl.get("context.showHidden"),
|
case ContextMenuType.View:
|
||||||
canCheck: true,
|
return [
|
||||||
checked: Boolean(this.props.filter & FilterType.ShowHidden),
|
{
|
||||||
onClick: () => this.props.toggleFilter(FilterType.ShowHidden)
|
key: "section_1",
|
||||||
}
|
itemType: ContextualMenuItemType.Section,
|
||||||
]
|
sectionProps: {
|
||||||
case ContextMenuType.Group: return [
|
title: intl.get("context.view"),
|
||||||
{
|
bottomDivider: true,
|
||||||
key: "markAllRead",
|
items: [
|
||||||
text: intl.get("nav.markAllRead"),
|
{
|
||||||
iconProps: { iconName: "CheckMark" },
|
key: "cardView",
|
||||||
onClick: () => this.props.markAllRead(this.props.sids)
|
text: intl.get("context.cardView"),
|
||||||
},
|
iconProps: { iconName: "GridViewMedium" },
|
||||||
{
|
canCheck: true,
|
||||||
key: "refresh",
|
checked:
|
||||||
text: intl.get("nav.refresh"),
|
this.props.viewType === ViewType.Cards,
|
||||||
iconProps: { iconName: "Sync" },
|
onClick: () =>
|
||||||
onClick: () => this.props.fetchItems(this.props.sids)
|
this.props.switchView(ViewType.Cards),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "manage",
|
key: "listView",
|
||||||
text: intl.get("context.manageSources"),
|
text: intl.get("context.listView"),
|
||||||
iconProps: { iconName: "Settings" },
|
iconProps: { iconName: "BacklogList" },
|
||||||
onClick: () => this.props.settings(this.props.sids)
|
canCheck: true,
|
||||||
}
|
checked:
|
||||||
]
|
this.props.viewType === ViewType.List,
|
||||||
case ContextMenuType.MarkRead: return [
|
onClick: () =>
|
||||||
{
|
this.props.switchView(ViewType.List),
|
||||||
key: "section_1",
|
},
|
||||||
itemType: ContextualMenuItemType.Section,
|
{
|
||||||
sectionProps: {
|
key: "magazineView",
|
||||||
title: intl.get("nav.markAllRead"),
|
text: intl.get("context.magazineView"),
|
||||||
items: [
|
iconProps: { iconName: "Articles" },
|
||||||
{
|
canCheck: true,
|
||||||
key: "all",
|
checked:
|
||||||
text: intl.get("allArticles"),
|
this.props.viewType ===
|
||||||
iconProps: { iconName: "ReceiptCheck" },
|
ViewType.Magazine,
|
||||||
onClick: () => this.props.markAllRead()
|
onClick: () =>
|
||||||
},
|
this.props.switchView(
|
||||||
{
|
ViewType.Magazine
|
||||||
key: "1d",
|
),
|
||||||
text: intl.get("app.daysAgo", { days: 1 }),
|
},
|
||||||
onClick: () => {
|
{
|
||||||
let date = new Date()
|
key: "compactView",
|
||||||
date.setTime(date.getTime() - 86400000)
|
text: intl.get("context.compactView"),
|
||||||
this.props.markAllRead(null, date)
|
iconProps: { iconName: "BulletedList" },
|
||||||
}
|
canCheck: true,
|
||||||
},
|
checked:
|
||||||
{
|
this.props.viewType ===
|
||||||
key: "3d",
|
ViewType.Compact,
|
||||||
text: intl.get("app.daysAgo", { days: 3 }),
|
onClick: () =>
|
||||||
onClick: () => {
|
this.props.switchView(ViewType.Compact),
|
||||||
let date = new Date()
|
},
|
||||||
date.setTime(date.getTime() - 3 * 86400000)
|
],
|
||||||
this.props.markAllRead(null, date)
|
},
|
||||||
}
|
},
|
||||||
},
|
{
|
||||||
{
|
key: "section_2",
|
||||||
key: "7d",
|
itemType: ContextualMenuItemType.Section,
|
||||||
text: intl.get("app.daysAgo", { days: 7 }),
|
sectionProps: {
|
||||||
onClick: () => {
|
title: intl.get("context.filter"),
|
||||||
let date = new Date()
|
bottomDivider: true,
|
||||||
date.setTime(date.getTime() - 7 * 86400000)
|
items: [
|
||||||
this.props.markAllRead(null, date)
|
{
|
||||||
}
|
key: "allArticles",
|
||||||
}
|
text: intl.get("allArticles"),
|
||||||
]
|
iconProps: { iconName: "ClearFilter" },
|
||||||
}
|
canCheck: true,
|
||||||
}
|
checked:
|
||||||
]
|
(this.props.filter &
|
||||||
default: return []
|
~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 : (
|
return this.props.type == ContextMenuType.Hidden ? null : (
|
||||||
<ContextualMenu
|
<ContextualMenu
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
items={this.getItems()}
|
items={this.getItems()}
|
||||||
target={this.props.event || this.props.position && {left: this.props.position[0], top: this.props.position[1]}}
|
target={
|
||||||
onDismiss={this.props.close} />
|
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 intl from "react-intl-universal"
|
||||||
import { FeedProps } from "./feed"
|
import { FeedProps } from "./feed"
|
||||||
import DefaultCard from "../cards/default-card"
|
import DefaultCard from "../cards/default-card"
|
||||||
import { PrimaryButton, FocusZone } from 'office-ui-fabric-react';
|
import { PrimaryButton, FocusZone } from "office-ui-fabric-react"
|
||||||
import { RSSItem } from "../../scripts/models/item";
|
import { RSSItem } from "../../scripts/models/item"
|
||||||
import { List, AnimationClassNames } from "@fluentui/react";
|
import { List, AnimationClassNames } from "@fluentui/react"
|
||||||
|
|
||||||
class CardsFeed extends React.Component<FeedProps> {
|
class CardsFeed extends React.Component<FeedProps> {
|
||||||
observer: ResizeObserver
|
observer: ResizeObserver
|
||||||
@ -12,12 +12,17 @@ class CardsFeed extends React.Component<FeedProps> {
|
|||||||
|
|
||||||
updateWindowSize = (entries: ResizeObserverEntry[]) => {
|
updateWindowSize = (entries: ResizeObserverEntry[]) => {
|
||||||
if (entries) {
|
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() {
|
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 = new ResizeObserver(this.updateWindowSize)
|
||||||
this.observer.observe(document.querySelector(".main"))
|
this.observer.observe(document.querySelector(".main"))
|
||||||
}
|
}
|
||||||
@ -31,33 +36,39 @@ class CardsFeed extends React.Component<FeedProps> {
|
|||||||
return elemPerRow * rows
|
return elemPerRow * rows
|
||||||
}
|
}
|
||||||
getPageHeight = () => {
|
getPageHeight = () => {
|
||||||
return this.state.height + (304 - this.state.height % 304)
|
return this.state.height + (304 - (this.state.height % 304))
|
||||||
}
|
}
|
||||||
|
|
||||||
flexFixItems = () => {
|
flexFixItems = () => {
|
||||||
let elemPerRow = Math.floor(this.state.width / 280)
|
let elemPerRow = Math.floor(this.state.width / 280)
|
||||||
let elemLastRow = this.props.items.length % elemPerRow
|
let elemLastRow = this.props.items.length % elemPerRow
|
||||||
let items = [ ...this.props.items ]
|
let items = [...this.props.items]
|
||||||
for (let i = 0; i < (elemPerRow - elemLastRow); i += 1) items.push(null)
|
for (let i = 0; i < elemPerRow - elemLastRow; i += 1) items.push(null)
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
onRenderItem = (item: RSSItem, index: number) => item ? (
|
onRenderItem = (item: RSSItem, index: number) =>
|
||||||
<DefaultCard
|
item ? (
|
||||||
feedId={this.props.feed._id}
|
<DefaultCard
|
||||||
key={item._id}
|
feedId={this.props.feed._id}
|
||||||
item={item}
|
key={item._id}
|
||||||
source={this.props.sourceMap[item.source]}
|
item={item}
|
||||||
filter={this.props.filter}
|
source={this.props.sourceMap[item.source]}
|
||||||
shortcuts={this.props.shortcuts}
|
filter={this.props.filter}
|
||||||
markRead={this.props.markRead}
|
shortcuts={this.props.shortcuts}
|
||||||
contextMenu={this.props.contextMenu}
|
markRead={this.props.markRead}
|
||||||
showItem={this.props.showItem} />
|
contextMenu={this.props.contextMenu}
|
||||||
) : (<div className="flex-fix" key={"f-"+index}></div>)
|
showItem={this.props.showItem}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex-fix" key={"f-" + index}></div>
|
||||||
|
)
|
||||||
|
|
||||||
canFocusChild = (el: HTMLElement) => {
|
canFocusChild = (el: HTMLElement) => {
|
||||||
if (el.id === "load-more") {
|
if (el.id === "load-more") {
|
||||||
const container = document.getElementById("refocus")
|
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
|
if (!result) container.scrollTop += 100
|
||||||
return result
|
return result
|
||||||
} else {
|
} else {
|
||||||
@ -66,35 +77,42 @@ class CardsFeed extends React.Component<FeedProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return this.props.feed.loaded && (
|
return (
|
||||||
<FocusZone as="div"
|
this.props.feed.loaded && (
|
||||||
id="refocus"
|
<FocusZone
|
||||||
className="cards-feed-container"
|
as="div"
|
||||||
shouldReceiveFocus={this.canFocusChild}
|
id="refocus"
|
||||||
data-is-scrollable>
|
className="cards-feed-container"
|
||||||
<List
|
shouldReceiveFocus={this.canFocusChild}
|
||||||
className={AnimationClassNames.slideUpIn10}
|
data-is-scrollable>
|
||||||
items={this.flexFixItems()}
|
<List
|
||||||
onRenderCell={this.onRenderItem}
|
className={AnimationClassNames.slideUpIn10}
|
||||||
getItemCountForPage={this.getItemCountForPage}
|
items={this.flexFixItems()}
|
||||||
getPageHeight={this.getPageHeight}
|
onRenderCell={this.onRenderItem}
|
||||||
ignoreScrollingState
|
getItemCountForPage={this.getItemCountForPage}
|
||||||
usePageCache />
|
getPageHeight={this.getPageHeight}
|
||||||
{
|
ignoreScrollingState
|
||||||
(this.props.feed.loaded && !this.props.feed.allLoaded)
|
usePageCache
|
||||||
? <div className="load-more-wrapper"><PrimaryButton
|
/>
|
||||||
id="load-more"
|
{this.props.feed.loaded && !this.props.feed.allLoaded ? (
|
||||||
text={intl.get("loadMore")}
|
<div className="load-more-wrapper">
|
||||||
disabled={this.props.feed.loading}
|
<PrimaryButton
|
||||||
onClick={() => this.props.loadMore(this.props.feed)} /></div>
|
id="load-more"
|
||||||
: null
|
text={intl.get("loadMore")}
|
||||||
}
|
disabled={this.props.feed.loading}
|
||||||
{ this.props.items.length === 0 && (
|
onClick={() =>
|
||||||
<div className="empty">{intl.get("article.empty")}</div>
|
this.props.loadMore(this.props.feed)
|
||||||
)}
|
}
|
||||||
</FocusZone>
|
/>
|
||||||
|
</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
|
showItem: (fid: string, item: RSSItem) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Feed extends React.Component<FeedProps> {
|
export class Feed extends React.Component<FeedProps> {
|
||||||
render() {
|
render() {
|
||||||
switch (this.props.viewType) {
|
switch (this.props.viewType) {
|
||||||
case (ViewType.Cards): return (
|
case ViewType.Cards:
|
||||||
<CardsFeed {...this.props} />
|
return <CardsFeed {...this.props} />
|
||||||
)
|
case ViewType.Magazine:
|
||||||
case (ViewType.Magazine):
|
case ViewType.Compact:
|
||||||
case (ViewType.Compact):
|
case ViewType.List:
|
||||||
case (ViewType.List): return (
|
return <ListFeed {...this.props} />
|
||||||
<ListFeed {...this.props} />
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,27 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import intl from "react-intl-universal"
|
import intl from "react-intl-universal"
|
||||||
import { FeedProps } from "./feed"
|
import { FeedProps } from "./feed"
|
||||||
import { PrimaryButton, FocusZone, FocusZoneDirection, List } from 'office-ui-fabric-react';
|
import {
|
||||||
import { RSSItem } from "../../scripts/models/item";
|
PrimaryButton,
|
||||||
import { AnimationClassNames } from "@fluentui/react";
|
FocusZone,
|
||||||
import { ViewType } from "../../schema-types";
|
FocusZoneDirection,
|
||||||
import ListCard from "../cards/list-card";
|
List,
|
||||||
import MagazineCard from "../cards/magazine-card";
|
} from "office-ui-fabric-react"
|
||||||
import CompactCard from "../cards/compact-card";
|
import { RSSItem } from "../../scripts/models/item"
|
||||||
import { Card } from "../cards/card";
|
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> {
|
class ListFeed extends React.Component<FeedProps> {
|
||||||
onRenderItem = (item: RSSItem) => {
|
onRenderItem = (item: RSSItem) => {
|
||||||
const props = {
|
const props = {
|
||||||
feedId: this.props.feed._id,
|
feedId: this.props.feed._id,
|
||||||
key: item._id,
|
key: item._id,
|
||||||
item: item,
|
item: item,
|
||||||
source: this.props.sourceMap[item.source],
|
source: this.props.sourceMap[item.source],
|
||||||
filter: this.props.filter,
|
filter: this.props.filter,
|
||||||
viewConfigs: this.props.viewConfigs,
|
viewConfigs: this.props.viewConfigs,
|
||||||
shortcuts: this.props.shortcuts,
|
shortcuts: this.props.shortcuts,
|
||||||
@ -24,29 +29,40 @@ class ListFeed extends React.Component<FeedProps> {
|
|||||||
contextMenu: this.props.contextMenu,
|
contextMenu: this.props.contextMenu,
|
||||||
showItem: this.props.showItem,
|
showItem: this.props.showItem,
|
||||||
} as Card.Props
|
} 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
|
props.selected = true
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (this.props.viewType) {
|
switch (this.props.viewType) {
|
||||||
case (ViewType.Magazine): return <MagazineCard {...props} />
|
case ViewType.Magazine:
|
||||||
case (ViewType.Compact): return <CompactCard {...props} />
|
return <MagazineCard {...props} />
|
||||||
default: return <ListCard {...props} />
|
case ViewType.Compact:
|
||||||
|
return <CompactCard {...props} />
|
||||||
|
default:
|
||||||
|
return <ListCard {...props} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getClassName = () => {
|
getClassName = () => {
|
||||||
switch (this.props.viewType) {
|
switch (this.props.viewType) {
|
||||||
case (ViewType.Magazine): return "magazine-feed"
|
case ViewType.Magazine:
|
||||||
case (ViewType.Compact): return "compact-feed"
|
return "magazine-feed"
|
||||||
default: return "list-feed"
|
case ViewType.Compact:
|
||||||
|
return "compact-feed"
|
||||||
|
default:
|
||||||
|
return "list-feed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
canFocusChild = (el: HTMLElement) => {
|
canFocusChild = (el: HTMLElement) => {
|
||||||
if (el.id === "load-more") {
|
if (el.id === "load-more") {
|
||||||
const container = document.getElementById("refocus")
|
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
|
if (!result) container.scrollTop += 100
|
||||||
return result
|
return result
|
||||||
} else {
|
} else {
|
||||||
@ -55,34 +71,41 @@ class ListFeed extends React.Component<FeedProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return this.props.feed.loaded && (
|
return (
|
||||||
<FocusZone as="div"
|
this.props.feed.loaded && (
|
||||||
id="refocus"
|
<FocusZone
|
||||||
direction={FocusZoneDirection.vertical}
|
as="div"
|
||||||
className={this.getClassName()}
|
id="refocus"
|
||||||
shouldReceiveFocus={this.canFocusChild}
|
direction={FocusZoneDirection.vertical}
|
||||||
data-is-scrollable>
|
className={this.getClassName()}
|
||||||
<List
|
shouldReceiveFocus={this.canFocusChild}
|
||||||
className={AnimationClassNames.slideUpIn10}
|
data-is-scrollable>
|
||||||
items={this.props.items}
|
<List
|
||||||
onRenderCell={this.onRenderItem}
|
className={AnimationClassNames.slideUpIn10}
|
||||||
ignoreScrollingState
|
items={this.props.items}
|
||||||
usePageCache />
|
onRenderCell={this.onRenderItem}
|
||||||
{
|
ignoreScrollingState
|
||||||
(this.props.feed.loaded && !this.props.feed.allLoaded)
|
usePageCache
|
||||||
? <div className="load-more-wrapper"><PrimaryButton
|
/>
|
||||||
id="load-more"
|
{this.props.feed.loaded && !this.props.feed.allLoaded ? (
|
||||||
text={intl.get("loadMore")}
|
<div className="load-more-wrapper">
|
||||||
disabled={this.props.feed.loading}
|
<PrimaryButton
|
||||||
onClick={() => this.props.loadMore(this.props.feed)} /></div>
|
id="load-more"
|
||||||
: null
|
text={intl.get("loadMore")}
|
||||||
}
|
disabled={this.props.feed.loading}
|
||||||
{ this.props.items.length === 0 && (
|
onClick={() =>
|
||||||
<div className="empty">{intl.get("article.empty")}</div>
|
this.props.loadMore(this.props.feed)
|
||||||
)}
|
}
|
||||||
</FocusZone>
|
/>
|
||||||
|
</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 * as React from "react"
|
||||||
import intl from "react-intl-universal"
|
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 { AppLog, AppLogType } from "../scripts/models/app"
|
||||||
import Time from "./utils/time"
|
import Time from "./utils/time"
|
||||||
|
|
||||||
@ -13,46 +19,67 @@ type LogMenuProps = {
|
|||||||
|
|
||||||
function getLogIcon(log: AppLog) {
|
function getLogIcon(log: AppLog) {
|
||||||
switch (log.type) {
|
switch (log.type) {
|
||||||
case AppLogType.Info: return "Info"
|
case AppLogType.Info:
|
||||||
case AppLogType.Article: return "KnowledgeArticle"
|
return "Info"
|
||||||
default: return "Warning"
|
case AppLogType.Article:
|
||||||
|
return "KnowledgeArticle"
|
||||||
|
default:
|
||||||
|
return "Warning"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LogMenu extends React.Component<LogMenuProps> {
|
class LogMenu extends React.Component<LogMenuProps> {
|
||||||
activityItems = () => this.props.logs.map((l, i) => ({
|
activityItems = () =>
|
||||||
key: i,
|
this.props.logs
|
||||||
activityDescription: l.iid
|
.map((l, i) => ({
|
||||||
? <b><Link onClick={() => this.handleArticleClick(l)}>{l.title}</Link></b>
|
key: i,
|
||||||
: <b>{l.title}</b>,
|
activityDescription: l.iid ? (
|
||||||
comments: l.details,
|
<b>
|
||||||
activityIcon: <Icon iconName={getLogIcon(l)} />,
|
<Link onClick={() => this.handleArticleClick(l)}>
|
||||||
timeStamp: <Time date={l.time} />,
|
{l.title}
|
||||||
})).reverse()
|
</Link>
|
||||||
|
</b>
|
||||||
|
) : (
|
||||||
|
<b>{l.title}</b>
|
||||||
|
),
|
||||||
|
comments: l.details,
|
||||||
|
activityIcon: <Icon iconName={getLogIcon(l)} />,
|
||||||
|
timeStamp: <Time date={l.time} />,
|
||||||
|
}))
|
||||||
|
.reverse()
|
||||||
|
|
||||||
handleArticleClick = (log: AppLog) => {
|
handleArticleClick = (log: AppLog) => {
|
||||||
this.props.close()
|
this.props.close()
|
||||||
this.props.showItem(log.iid)
|
this.props.showItem(log.iid)
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
return this.props.display && (
|
return (
|
||||||
<Callout
|
this.props.display && (
|
||||||
target="#log-toggle"
|
<Callout
|
||||||
role="log-menu"
|
target="#log-toggle"
|
||||||
directionalHint={DirectionalHint.bottomCenter}
|
role="log-menu"
|
||||||
calloutWidth={320}
|
directionalHint={DirectionalHint.bottomCenter}
|
||||||
calloutMaxHeight={240}
|
calloutWidth={320}
|
||||||
onDismiss={this.props.close}
|
calloutMaxHeight={240}
|
||||||
>
|
onDismiss={this.props.close}>
|
||||||
{ this.props.logs.length == 0
|
{this.props.logs.length == 0 ? (
|
||||||
? <p style={{ textAlign: "center" }}>{intl.get("log.empty")}</p>
|
<p style={{ textAlign: "center" }}>
|
||||||
: this.activityItems().map((item => (
|
{intl.get("log.empty")}
|
||||||
<ActivityItem {...item} key={item.key} style={{ margin: 12 }} />
|
</p>
|
||||||
))) }
|
) : (
|
||||||
</Callout>
|
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"
|
import { AnimationClassNames, Stack, FocusZone } from "@fluentui/react"
|
||||||
|
|
||||||
export type MenuProps = {
|
export type MenuProps = {
|
||||||
status: boolean,
|
status: boolean
|
||||||
display: boolean,
|
display: boolean
|
||||||
selected: string,
|
selected: string
|
||||||
sources: SourceState,
|
sources: SourceState
|
||||||
groups: SourceGroup[],
|
groups: SourceGroup[]
|
||||||
searchOn: boolean,
|
searchOn: boolean
|
||||||
itemOn: boolean,
|
itemOn: boolean
|
||||||
toggleMenu: () => void,
|
toggleMenu: () => void
|
||||||
allArticles: (init?: boolean) => void,
|
allArticles: (init?: boolean) => void
|
||||||
selectSourceGroup: (group: SourceGroup, menuKey: string) => void,
|
selectSourceGroup: (group: SourceGroup, menuKey: string) => void
|
||||||
selectSource: (source: RSSSource) => void,
|
selectSource: (source: RSSSource) => void
|
||||||
groupContextMenu: (sids: number[], event: React.MouseEvent) => void,
|
groupContextMenu: (sids: number[], event: React.MouseEvent) => void
|
||||||
updateGroupExpansion: (event: React.MouseEvent<HTMLElement>, key: string, selected: string) => void,
|
updateGroupExpansion: (
|
||||||
toggleSearch: () => void,
|
event: React.MouseEvent<HTMLElement>,
|
||||||
|
key: string,
|
||||||
|
selected: string
|
||||||
|
) => void
|
||||||
|
toggleSearch: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Menu extends React.Component<MenuProps> {
|
export class Menu extends React.Component<MenuProps> {
|
||||||
countOverflow = (count: number) => count >= 1000 ? " 999+" : ` ${count}`
|
countOverflow = (count: number) => (count >= 1000 ? " 999+" : ` ${count}`)
|
||||||
|
|
||||||
getLinkGroups = (): INavLinkGroup[] => [
|
getLinkGroups = (): INavLinkGroup[] => [
|
||||||
{
|
{
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
name: intl.get("search"),
|
name: intl.get("search"),
|
||||||
ariaLabel: intl.get("search") + (this.props.searchOn ? " ✓" : " "),
|
ariaLabel:
|
||||||
|
intl.get("search") + (this.props.searchOn ? " ✓" : " "),
|
||||||
key: "search",
|
key: "search",
|
||||||
icon: "Search",
|
icon: "Search",
|
||||||
onClick: this.props.toggleSearch,
|
onClick: this.props.toggleSearch,
|
||||||
url: null
|
url: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: intl.get("allArticles"),
|
name: intl.get("allArticles"),
|
||||||
ariaLabel: intl.get("allArticles")
|
ariaLabel:
|
||||||
+ this.countOverflow(Object.values(this.props.sources).map(s => s.unreadCount).reduce((a, b) => a + b, 0)),
|
intl.get("allArticles") +
|
||||||
|
this.countOverflow(
|
||||||
|
Object.values(this.props.sources)
|
||||||
|
.map(s => s.unreadCount)
|
||||||
|
.reduce((a, b) => a + b, 0)
|
||||||
|
),
|
||||||
key: ALL,
|
key: ALL,
|
||||||
icon: "TextDocument",
|
icon: "TextDocument",
|
||||||
onClick: () => this.props.allArticles(this.props.selected !== ALL),
|
onClick: () =>
|
||||||
url: null
|
this.props.allArticles(this.props.selected !== ALL),
|
||||||
}
|
url: null,
|
||||||
]
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: intl.get("menu.subscriptions"),
|
name: intl.get("menu.subscriptions"),
|
||||||
links: this.props.groups.filter(g => g.sids.length > 0).map(g => {
|
links: this.props.groups
|
||||||
if (g.isMultiple) {
|
.filter(g => g.sids.length > 0)
|
||||||
let sources = g.sids.map(sid => this.props.sources[sid])
|
.map(g => {
|
||||||
return {
|
if (g.isMultiple) {
|
||||||
name: g.name,
|
let sources = g.sids.map(sid => this.props.sources[sid])
|
||||||
ariaLabel: g.name + this.countOverflow(sources.map(s => s.unreadCount).reduce((a, b) => a + b, 0)),
|
return {
|
||||||
key: "g-" + g.index,
|
name: g.name,
|
||||||
url: null,
|
ariaLabel:
|
||||||
isExpanded: g.expanded,
|
g.name +
|
||||||
onClick: () => this.props.selectSourceGroup(g, "g-" + g.index),
|
this.countOverflow(
|
||||||
links: sources.map(this.getSource)
|
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 => ({
|
getSource = (s: RSSSource): INavLink => ({
|
||||||
@ -76,16 +96,15 @@ export class Menu extends React.Component<MenuProps> {
|
|||||||
key: "s-" + s.sid,
|
key: "s-" + s.sid,
|
||||||
onClick: () => this.props.selectSource(s),
|
onClick: () => this.props.selectSource(s),
|
||||||
iconProps: s.iconurl ? this.getIconStyle(s.iconurl) : null,
|
iconProps: s.iconurl ? this.getIconStyle(s.iconurl) : null,
|
||||||
url: null
|
url: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
getIconStyle = (url: string) => ({
|
getIconStyle = (url: string) => ({
|
||||||
style: { width: 16 },
|
style: { width: 16 },
|
||||||
imageProps: {
|
imageProps: {
|
||||||
style: { width:"100%" },
|
style: { width: "100%" },
|
||||||
src: url
|
src: url,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
onContext = (item: INavLink, event: React.MouseEvent) => {
|
onContext = (item: INavLink, event: React.MouseEvent) => {
|
||||||
@ -104,37 +123,81 @@ export class Menu extends React.Component<MenuProps> {
|
|||||||
_onRenderLink = (link: INavLink): JSX.Element => {
|
_onRenderLink = (link: INavLink): JSX.Element => {
|
||||||
let count = link.ariaLabel.split(" ").pop()
|
let count = link.ariaLabel.split(" ").pop()
|
||||||
return (
|
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>
|
<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>
|
</Stack>
|
||||||
)
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
_onRenderGroupHeader = (group: INavLinkGroup): JSX.Element => {
|
_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() {
|
render() {
|
||||||
return this.props.status && (
|
return (
|
||||||
<div className={"menu-container" + (this.props.display ? " show" : "")} onClick={this.props.toggleMenu}>
|
this.props.status && (
|
||||||
<div className={"menu" + (this.props.itemOn ? " item-on" : "")} onClick={(e) => e.stopPropagation()}>
|
<div
|
||||||
<div className="btn-group">
|
className={
|
||||||
<a className="btn hide-wide" title={intl.get("menu.close")} onClick={this.props.toggleMenu}><Icon iconName="Back" /></a>
|
"menu-container" + (this.props.display ? " show" : "")
|
||||||
<a className="btn inline-block-wide" title={intl.get("menu.close")} onClick={this.props.toggleMenu}>
|
}
|
||||||
<Icon iconName={window.utils.platform === "darwin" ? "SidePanel" : "GlobalNavButton"} />
|
onClick={this.props.toggleMenu}>
|
||||||
</a>
|
<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>
|
</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>
|
||||||
</div>
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ type NavProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type NavState = {
|
type NavState = {
|
||||||
maximized: boolean,
|
maximized: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
class Nav extends React.Component<NavProps, NavState> {
|
class Nav extends React.Component<NavProps, NavState> {
|
||||||
@ -29,7 +29,7 @@ class Nav extends React.Component<NavProps, NavState> {
|
|||||||
this.setBodyFullscreenState(window.utils.isFullscreen())
|
this.setBodyFullscreenState(window.utils.isFullscreen())
|
||||||
window.utils.addWindowStateListener(this.windowStateListener)
|
window.utils.addWindowStateListener(this.windowStateListener)
|
||||||
this.state = {
|
this.state = {
|
||||||
maximized: window.utils.isMaximized()
|
maximized: window.utils.isMaximized(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,7 +87,8 @@ class Nav extends React.Component<NavProps, NavState> {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
document.addEventListener("keydown", this.navShortcutsHandler)
|
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() {
|
componentWillUnmount() {
|
||||||
document.removeEventListener("keydown", this.navShortcutsHandler)
|
document.removeEventListener("keydown", this.navShortcutsHandler)
|
||||||
@ -104,9 +105,12 @@ class Nav extends React.Component<NavProps, NavState> {
|
|||||||
window.utils.closeWindow()
|
window.utils.closeWindow()
|
||||||
}
|
}
|
||||||
|
|
||||||
canFetch = () => this.props.state.sourceInit && this.props.state.feedInit
|
canFetch = () =>
|
||||||
&& !this.props.state.syncing && !this.props.state.fetchingItems
|
this.props.state.sourceInit &&
|
||||||
fetching = () => !this.canFetch() ? " fetching" : ""
|
this.props.state.feedInit &&
|
||||||
|
!this.props.state.syncing &&
|
||||||
|
!this.props.state.fetchingItems
|
||||||
|
fetching = () => (!this.canFetch() ? " fetching" : "")
|
||||||
getClassNames = () => {
|
getClassNames = () => {
|
||||||
const classNames = new Array<string>()
|
const classNames = new Array<string>()
|
||||||
if (this.props.state.settings.display) classNames.push("hide-btns")
|
if (this.props.state.settings.display) classNames.push("hide-btns")
|
||||||
@ -126,7 +130,7 @@ class Nav extends React.Component<NavProps, NavState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getProgress = () => {
|
getProgress = () => {
|
||||||
return this.props.state.fetchingTotal > 0
|
return this.props.state.fetchingTotal > 0
|
||||||
? this.props.state.fetchingProgress / this.props.state.fetchingTotal
|
? this.props.state.fetchingProgress / this.props.state.fetchingTotal
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
@ -135,73 +139,112 @@ class Nav extends React.Component<NavProps, NavState> {
|
|||||||
return (
|
return (
|
||||||
<nav className={this.getClassNames()}>
|
<nav className={this.getClassNames()}>
|
||||||
<div className="btn-group">
|
<div className="btn-group">
|
||||||
<a className="btn hide-wide"
|
<a
|
||||||
title={intl.get("nav.menu")}
|
className="btn hide-wide"
|
||||||
|
title={intl.get("nav.menu")}
|
||||||
onClick={this.props.menu}>
|
onClick={this.props.menu}>
|
||||||
<Icon iconName={window.utils.platform === "darwin" ? "SidePanel" : "GlobalNavButton"} />
|
<Icon
|
||||||
|
iconName={
|
||||||
|
window.utils.platform === "darwin"
|
||||||
|
? "SidePanel"
|
||||||
|
: "GlobalNavButton"
|
||||||
|
}
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<span className="title">{this.props.state.title}</span>
|
<span className="title">{this.props.state.title}</span>
|
||||||
<div className="btn-group" style={{float:"right"}}>
|
<div className="btn-group" style={{ float: "right" }}>
|
||||||
<a className={"btn"+this.fetching()}
|
<a
|
||||||
onClick={this.fetch}
|
className={"btn" + this.fetching()}
|
||||||
|
onClick={this.fetch}
|
||||||
title={intl.get("nav.refresh")}>
|
title={intl.get("nav.refresh")}>
|
||||||
<Icon iconName="Refresh" />
|
<Icon iconName="Refresh" />
|
||||||
</a>
|
</a>
|
||||||
<a className="btn"
|
<a
|
||||||
|
className="btn"
|
||||||
id="mark-all-toggle"
|
id="mark-all-toggle"
|
||||||
onClick={this.props.markAllRead}
|
onClick={this.props.markAllRead}
|
||||||
title={intl.get("nav.markAllRead")}
|
title={intl.get("nav.markAllRead")}
|
||||||
onMouseDown={e => {
|
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" />
|
<Icon iconName="InboxCheck" />
|
||||||
</a>
|
</a>
|
||||||
<a className="btn"
|
<a
|
||||||
id="log-toggle"
|
className="btn"
|
||||||
title={intl.get("nav.notifications")}
|
id="log-toggle"
|
||||||
|
title={intl.get("nav.notifications")}
|
||||||
onClick={this.props.logs}>
|
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>
|
||||||
<a className="btn"
|
<a
|
||||||
id="view-toggle"
|
className="btn"
|
||||||
|
id="view-toggle"
|
||||||
title={intl.get("nav.view")}
|
title={intl.get("nav.view")}
|
||||||
onClick={this.props.views}
|
onClick={this.props.views}
|
||||||
onMouseDown={e => {
|
onMouseDown={e => {
|
||||||
if (this.props.state.contextMenu.event === "#view-toggle") e.stopPropagation()}}>
|
if (
|
||||||
<Icon iconName="View" /></a>
|
this.props.state.contextMenu.event ===
|
||||||
<a className="btn"
|
"#view-toggle"
|
||||||
|
)
|
||||||
|
e.stopPropagation()
|
||||||
|
}}>
|
||||||
|
<Icon iconName="View" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
className="btn"
|
||||||
title={intl.get("nav.settings")}
|
title={intl.get("nav.settings")}
|
||||||
onClick={this.props.settings}>
|
onClick={this.props.settings}>
|
||||||
<Icon iconName="Settings" />
|
<Icon iconName="Settings" />
|
||||||
</a>
|
</a>
|
||||||
<span className="seperator"></span>
|
<span className="seperator"></span>
|
||||||
<a className="btn system"
|
<a
|
||||||
title={intl.get("nav.minimize")}
|
className="btn system"
|
||||||
onClick={this.minimize}
|
title={intl.get("nav.minimize")}
|
||||||
style={{fontSize: 12}}>
|
onClick={this.minimize}
|
||||||
|
style={{ fontSize: 12 }}>
|
||||||
<Icon iconName="Remove" />
|
<Icon iconName="Remove" />
|
||||||
</a>
|
</a>
|
||||||
<a className="btn system"
|
<a
|
||||||
title={intl.get("nav.maximize")}
|
className="btn system"
|
||||||
|
title={intl.get("nav.maximize")}
|
||||||
onClick={this.maximize}>
|
onClick={this.maximize}>
|
||||||
{this.state.maximized
|
{this.state.maximized ? (
|
||||||
? <Icon iconName="ChromeRestore" style={{fontSize: 11}} />
|
<Icon
|
||||||
: <Icon iconName="Checkbox" style={{fontSize: 10}} />}
|
iconName="ChromeRestore"
|
||||||
|
style={{ fontSize: 11 }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Icon
|
||||||
|
iconName="Checkbox"
|
||||||
|
style={{ fontSize: 10 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</a>
|
</a>
|
||||||
<a className="btn system close"
|
<a
|
||||||
|
className="btn system close"
|
||||||
title={intl.get("close")}
|
title={intl.get("close")}
|
||||||
onClick={this.close}>
|
onClick={this.close}>
|
||||||
<Icon iconName="Cancel" />
|
<Icon iconName="Cancel" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{!this.canFetch() &&
|
{!this.canFetch() && (
|
||||||
<ProgressIndicator
|
<ProgressIndicator
|
||||||
className="progress"
|
className="progress"
|
||||||
percentComplete={this.getProgress()} />
|
percentComplete={this.getProgress()}
|
||||||
}
|
/>
|
||||||
|
)}
|
||||||
</nav>
|
</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)
|
prevItem = (event: React.MouseEvent) => this.offsetItem(event, -1)
|
||||||
nextItem = (event: React.MouseEvent) => this.offsetItem(event, 1)
|
nextItem = (event: React.MouseEvent) => this.offsetItem(event, 1)
|
||||||
|
|
||||||
render = () => this.props.viewType !== ViewType.List
|
render = () =>
|
||||||
? (
|
this.props.viewType !== ViewType.List ? (
|
||||||
<>
|
<>
|
||||||
{this.props.settingsOn ? null :
|
{this.props.settingsOn ? null : (
|
||||||
<div key="card" className={"main" + (this.props.menuOn ? " menu-on" : "")}>
|
<div
|
||||||
<ArticleSearch />
|
key="card"
|
||||||
{this.props.feeds.map(fid => (
|
className={
|
||||||
<FeedContainer viewType={this.props.viewType} feedId={fid} key={fid + this.props.viewType} />
|
"main" + (this.props.menuOn ? " menu-on" : "")
|
||||||
))}
|
}>
|
||||||
</div>}
|
<ArticleSearch />
|
||||||
{this.props.itemId && (
|
{this.props.feeds.map(fid => (
|
||||||
<FocusTrapZone
|
<FeedContainer
|
||||||
disabled={this.props.contextOn}
|
viewType={this.props.viewType}
|
||||||
ignoreExternalFocusing={true}
|
feedId={fid}
|
||||||
isClickableOutsideFocusTrap={true}
|
key={fid + this.props.viewType}
|
||||||
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>
|
||||||
)}
|
)}
|
||||||
</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 * as React from "react"
|
||||||
import { connect } from 'react-redux'
|
import { connect } from "react-redux"
|
||||||
import { ContextMenuContainer } from "../containers/context-menu-container"
|
import { ContextMenuContainer } from "../containers/context-menu-container"
|
||||||
import { closeContextMenu } from "../scripts/models/app"
|
import { closeContextMenu } from "../scripts/models/app"
|
||||||
import PageContainer from "../containers/page-container"
|
import PageContainer from "../containers/page-container"
|
||||||
@ -9,18 +9,20 @@ import LogMenuContainer from "../containers/log-menu-container"
|
|||||||
import SettingsContainer from "../containers/settings-container"
|
import SettingsContainer from "../containers/settings-container"
|
||||||
import { RootState } from "../scripts/reducer"
|
import { RootState } from "../scripts/reducer"
|
||||||
|
|
||||||
const Root = ({ locale, dispatch }) => locale && (
|
const Root = ({ locale, dispatch }) =>
|
||||||
<div id="root"
|
locale && (
|
||||||
key={locale}
|
<div
|
||||||
onMouseDown={() => dispatch(closeContextMenu())}>
|
id="root"
|
||||||
<NavContainer />
|
key={locale}
|
||||||
<PageContainer />
|
onMouseDown={() => dispatch(closeContextMenu())}>
|
||||||
<LogMenuContainer />
|
<NavContainer />
|
||||||
<MenuContainer />
|
<PageContainer />
|
||||||
<SettingsContainer />
|
<LogMenuContainer />
|
||||||
<ContextMenuContainer />
|
<MenuContainer />
|
||||||
</div>
|
<SettingsContainer />
|
||||||
)
|
<ContextMenuContainer />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
const getLocale = (state: RootState) => ({ locale: state.app.locale })
|
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"
|
import { initTouchBarWithTexts } from "../scripts/utils"
|
||||||
|
|
||||||
type SettingsProps = {
|
type SettingsProps = {
|
||||||
display: boolean,
|
display: boolean
|
||||||
blocked: boolean,
|
blocked: boolean
|
||||||
exitting: boolean,
|
exitting: boolean
|
||||||
close: () => void
|
close: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
class Settings extends React.Component<SettingsProps> {
|
class Settings extends React.Component<SettingsProps> {
|
||||||
constructor(props){
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,7 +30,8 @@ class Settings extends React.Component<SettingsProps> {
|
|||||||
componentDidUpdate = (prevProps: SettingsProps) => {
|
componentDidUpdate = (prevProps: SettingsProps) => {
|
||||||
if (this.props.display !== prevProps.display) {
|
if (this.props.display !== prevProps.display) {
|
||||||
if (this.props.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)
|
document.body.addEventListener("keydown", this.onKeyDown)
|
||||||
} else {
|
} else {
|
||||||
if (window.utils.platform === "darwin") initTouchBarWithTexts()
|
if (window.utils.platform === "darwin") initTouchBarWithTexts()
|
||||||
@ -39,42 +40,71 @@ class Settings extends React.Component<SettingsProps> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render = () => this.props.display && (
|
render = () =>
|
||||||
<div className="settings-container">
|
this.props.display && (
|
||||||
<div className="btn-group" style={{position: "absolute", top: 70, left: "calc(50% - 404px)"}}>
|
<div className="settings-container">
|
||||||
<a className={"btn" + (this.props.exitting ? " disabled" : "")} title={intl.get("settings.exit")} onClick={this.props.close}>
|
<div
|
||||||
<Icon iconName="Back" />
|
className="btn-group"
|
||||||
</a>
|
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>
|
||||||
<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 = () => (
|
render = () => (
|
||||||
<div className="tab-body">
|
<div className="tab-body">
|
||||||
<Stack className="settings-about" horizontalAlign="center">
|
<Stack className="settings-about" horizontalAlign="center">
|
||||||
<img src="icons/logo.svg" style={{width: 120, height: 120}} />
|
<img src="icons/logo.svg" style={{ width: 120, height: 120 }} />
|
||||||
<h3 style={{fontWeight: 600}}>Fluent Reader</h3>
|
<h3 style={{ fontWeight: 600 }}>Fluent Reader</h3>
|
||||||
<small>{intl.get("settings.version")} {window.utils.getVersion()}</small>
|
<small>
|
||||||
<p className="settings-hint">Copyright © 2020 Haoyuan Liu. All rights reserved.</p>
|
{intl.get("settings.version")} {window.utils.getVersion()}
|
||||||
<Stack horizontal horizontalAlign="center" tokens={{childrenGap: 12}}>
|
</small>
|
||||||
<small><Link onClick={() => window.utils.openExternal("https://github.com/yang991178/fluent-reader/wiki/Support#keyboard-shortcuts")}>{intl.get("settings.shortcuts")}</Link></small>
|
<p className="settings-hint">
|
||||||
<small><Link onClick={() => window.utils.openExternal("https://github.com/yang991178/fluent-reader")}>{intl.get("settings.openSource")}</Link></small>
|
Copyright © 2020 Haoyuan Liu. All rights reserved.
|
||||||
<small><Link onClick={() => window.utils.openExternal("https://github.com/yang991178/fluent-reader/issues")}>{intl.get("settings.feedback")}</Link></small>
|
</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>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AboutTab
|
export default AboutTab
|
||||||
|
@ -1,9 +1,30 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import intl from "react-intl-universal"
|
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 { ThemeSettings, SearchEngines } from "../../schema-types"
|
||||||
import { getThemeSettings, setThemeSettings, exportAll } from "../../scripts/settings"
|
import {
|
||||||
import { Stack, Label, Toggle, TextField, DefaultButton, ChoiceGroup, IChoiceGroupOption, loadTheme, Dropdown, IDropdownOption, PrimaryButton } from "@fluentui/react"
|
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"
|
import DangerButton from "../utils/danger-button"
|
||||||
|
|
||||||
type AppTabProps = {
|
type AppTabProps = {
|
||||||
@ -31,7 +52,7 @@ class AppTab extends React.Component<AppTabProps, AppTabState> {
|
|||||||
themeSettings: getThemeSettings(),
|
themeSettings: getThemeSettings(),
|
||||||
itemSize: null,
|
itemSize: null,
|
||||||
cacheSize: null,
|
cacheSize: null,
|
||||||
deleteIndex: null
|
deleteIndex: null,
|
||||||
}
|
}
|
||||||
this.getItemSize()
|
this.getItemSize()
|
||||||
this.getCacheSize()
|
this.getCacheSize()
|
||||||
@ -43,7 +64,7 @@ class AppTab extends React.Component<AppTabProps, AppTabState> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
getItemSize = () => {
|
getItemSize = () => {
|
||||||
calculateItemSize().then((size) => {
|
calculateItemSize().then(size => {
|
||||||
this.setState({ itemSize: byteToMB(size) })
|
this.setState({ itemSize: byteToMB(size) })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -53,11 +74,11 @@ class AppTab extends React.Component<AppTabProps, AppTabState> {
|
|||||||
this.getCacheSize()
|
this.getCacheSize()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
themeChoices = (): IChoiceGroupOption[] => [
|
themeChoices = (): IChoiceGroupOption[] => [
|
||||||
{ key: ThemeSettings.Default, text: intl.get("followSystem") },
|
{ key: ThemeSettings.Default, text: intl.get("followSystem") },
|
||||||
{ key: ThemeSettings.Light, text: intl.get("app.lightTheme") },
|
{ 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[] => [
|
fetchIntervalOptions = (): IDropdownOption[] => [
|
||||||
@ -73,12 +94,16 @@ class AppTab extends React.Component<AppTabProps, AppTabState> {
|
|||||||
this.props.setFetchInterval(item.key as number)
|
this.props.setFetchInterval(item.key as number)
|
||||||
}
|
}
|
||||||
|
|
||||||
searchEngineOptions = (): IDropdownOption[] => [
|
searchEngineOptions = (): IDropdownOption[] =>
|
||||||
SearchEngines.Google, SearchEngines.Bing, SearchEngines.Baidu, SearchEngines.DuckDuckGo
|
[
|
||||||
].map(engine => ({
|
SearchEngines.Google,
|
||||||
key: engine,
|
SearchEngines.Bing,
|
||||||
text: getSearchEngineName(engine)
|
SearchEngines.Baidu,
|
||||||
}))
|
SearchEngines.DuckDuckGo,
|
||||||
|
].map(engine => ({
|
||||||
|
key: engine,
|
||||||
|
text: getSearchEngineName(engine),
|
||||||
|
}))
|
||||||
onSearchEngineChanged = (item: IDropdownOption) => {
|
onSearchEngineChanged = (item: IDropdownOption) => {
|
||||||
window.settings.setSearchEngine(item.key as number)
|
window.settings.setSearchEngine(item.key as number)
|
||||||
}
|
}
|
||||||
@ -97,7 +122,8 @@ class AppTab extends React.Component<AppTabProps, AppTabState> {
|
|||||||
|
|
||||||
confirmDelete = () => {
|
confirmDelete = () => {
|
||||||
this.setState({ itemSize: null })
|
this.setState({ itemSize: null })
|
||||||
this.props.deleteArticles(parseInt(this.state.deleteIndex))
|
this.props
|
||||||
|
.deleteArticles(parseInt(this.state.deleteIndex))
|
||||||
.then(() => this.getItemSize())
|
.then(() => this.getItemSize())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,21 +147,22 @@ class AppTab extends React.Component<AppTabProps, AppTabState> {
|
|||||||
|
|
||||||
toggleStatus = () => {
|
toggleStatus = () => {
|
||||||
window.settings.toggleProxyStatus()
|
window.settings.toggleProxyStatus()
|
||||||
this.setState({
|
this.setState({
|
||||||
pacStatus: window.settings.getProxyStatus(),
|
pacStatus: window.settings.getProxyStatus(),
|
||||||
pacUrl: window.settings.getProxy()
|
pacUrl: window.settings.getProxy(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleInputChange = (event) => {
|
handleInputChange = event => {
|
||||||
const name: string = event.target.name
|
const name: string = event.target.name
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.setState({[name]: event.target.value.trim()})
|
this.setState({ [name]: event.target.value.trim() })
|
||||||
}
|
}
|
||||||
|
|
||||||
setUrl = (event: React.FormEvent) => {
|
setUrl = (event: React.FormEvent) => {
|
||||||
event.preventDefault()
|
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) => {
|
onThemeChange = (_, option: IChoiceGroupOption) => {
|
||||||
@ -148,11 +175,14 @@ class AppTab extends React.Component<AppTabProps, AppTabState> {
|
|||||||
<Label>{intl.get("app.language")}</Label>
|
<Label>{intl.get("app.language")}</Label>
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<Stack.Item>
|
<Stack.Item>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
defaultSelectedKey={window.settings.getLocaleSettings()}
|
defaultSelectedKey={window.settings.getLocaleSettings()}
|
||||||
options={this.languageOptions()}
|
options={this.languageOptions()}
|
||||||
onChanged={option => this.props.setLanguage(String(option.key))}
|
onChanged={option =>
|
||||||
style={{width: 200}} />
|
this.props.setLanguage(String(option.key))
|
||||||
|
}
|
||||||
|
style={{ width: 200 }}
|
||||||
|
/>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
@ -160,27 +190,30 @@ class AppTab extends React.Component<AppTabProps, AppTabState> {
|
|||||||
label={intl.get("app.theme")}
|
label={intl.get("app.theme")}
|
||||||
options={this.themeChoices()}
|
options={this.themeChoices()}
|
||||||
onChange={this.onThemeChange}
|
onChange={this.onThemeChange}
|
||||||
selectedKey={this.state.themeSettings} />
|
selectedKey={this.state.themeSettings}
|
||||||
|
/>
|
||||||
|
|
||||||
<Label>{intl.get("app.fetchInterval")}</Label>
|
<Label>{intl.get("app.fetchInterval")}</Label>
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<Stack.Item>
|
<Stack.Item>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
defaultSelectedKey={window.settings.getFetchInterval()}
|
defaultSelectedKey={window.settings.getFetchInterval()}
|
||||||
options={this.fetchIntervalOptions()}
|
options={this.fetchIntervalOptions()}
|
||||||
onChanged={this.onFetchIntervalChanged}
|
onChanged={this.onFetchIntervalChanged}
|
||||||
style={{width: 200}} />
|
style={{ width: 200 }}
|
||||||
|
/>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Label>{intl.get("searchEngine.name")}</Label>
|
<Label>{intl.get("searchEngine.name")}</Label>
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<Stack.Item>
|
<Stack.Item>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
defaultSelectedKey={window.settings.getSearchEngine()}
|
defaultSelectedKey={window.settings.getSearchEngine()}
|
||||||
options={this.searchEngineOptions()}
|
options={this.searchEngineOptions()}
|
||||||
onChanged={this.onSearchEngineChanged}
|
onChanged={this.onSearchEngineChanged}
|
||||||
style={{width: 200}} />
|
style={{ width: 200 }}
|
||||||
|
/>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
@ -189,70 +222,100 @@ class AppTab extends React.Component<AppTabProps, AppTabState> {
|
|||||||
<Label>{intl.get("app.enableProxy")}</Label>
|
<Label>{intl.get("app.enableProxy")}</Label>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<Stack.Item>
|
<Stack.Item>
|
||||||
<Toggle checked={this.state.pacStatus} onChange={this.toggleStatus} />
|
<Toggle
|
||||||
|
checked={this.state.pacStatus}
|
||||||
|
onChange={this.toggleStatus}
|
||||||
|
/>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
</Stack>
|
</Stack>
|
||||||
{this.state.pacStatus && <form onSubmit={this.setUrl}>
|
{this.state.pacStatus && (
|
||||||
<Stack horizontal>
|
<form onSubmit={this.setUrl}>
|
||||||
<Stack.Item grow>
|
<Stack horizontal>
|
||||||
<TextField
|
<Stack.Item grow>
|
||||||
required
|
<TextField
|
||||||
onGetErrorMessage={v => urlTest(v.trim()) ? "" : intl.get("app.badUrl")}
|
required
|
||||||
placeholder={intl.get("app.pac")}
|
onGetErrorMessage={v =>
|
||||||
name="pacUrl"
|
urlTest(v.trim())
|
||||||
onChange={this.handleInputChange}
|
? ""
|
||||||
value={this.state.pacUrl} />
|
: intl.get("app.badUrl")
|
||||||
</Stack.Item>
|
}
|
||||||
<Stack.Item>
|
placeholder={intl.get("app.pac")}
|
||||||
<DefaultButton
|
name="pacUrl"
|
||||||
disabled={!urlTest(this.state.pacUrl)}
|
onChange={this.handleInputChange}
|
||||||
type="sumbit"
|
value={this.state.pacUrl}
|
||||||
text={intl.get("app.setPac")} />
|
/>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
</Stack>
|
<Stack.Item>
|
||||||
<span className="settings-hint up">
|
<DefaultButton
|
||||||
{intl.get("app.pacHint")}
|
disabled={!urlTest(this.state.pacUrl)}
|
||||||
</span>
|
type="sumbit"
|
||||||
</form>}
|
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>
|
<Label>{intl.get("app.cleanup")}</Label>
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<Stack.Item grow>
|
<Stack.Item grow>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
placeholder={intl.get("app.deleteChoices")}
|
placeholder={intl.get("app.deleteChoices")}
|
||||||
options={this.deleteOptions()}
|
options={this.deleteOptions()}
|
||||||
selectedKey={this.state.deleteIndex}
|
selectedKey={this.state.deleteIndex}
|
||||||
onChange={this.deleteChange} />
|
onChange={this.deleteChange}
|
||||||
|
/>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<Stack.Item>
|
<Stack.Item>
|
||||||
<DangerButton
|
<DangerButton
|
||||||
disabled={this.state.itemSize === null || this.state.deleteIndex === null}
|
disabled={
|
||||||
|
this.state.itemSize === null ||
|
||||||
|
this.state.deleteIndex === null
|
||||||
|
}
|
||||||
text={intl.get("app.confirmDelete")}
|
text={intl.get("app.confirmDelete")}
|
||||||
onClick={this.confirmDelete} />
|
onClick={this.confirmDelete}
|
||||||
|
/>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
</Stack>
|
</Stack>
|
||||||
<span className="settings-hint up">
|
<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>
|
</span>
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<Stack.Item>
|
<Stack.Item>
|
||||||
<DefaultButton
|
<DefaultButton
|
||||||
text={intl.get("app.cache")}
|
text={intl.get("app.cache")}
|
||||||
disabled={this.state.cacheSize === null || this.state.cacheSize === "0MB"}
|
disabled={
|
||||||
onClick={this.clearCache} />
|
this.state.cacheSize === null ||
|
||||||
|
this.state.cacheSize === "0MB"
|
||||||
|
}
|
||||||
|
onClick={this.clearCache}
|
||||||
|
/>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
</Stack>
|
</Stack>
|
||||||
<span className="settings-hint up">
|
<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>
|
</span>
|
||||||
|
|
||||||
<Label>{intl.get("app.data")}</Label>
|
<Label>{intl.get("app.data")}</Label>
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<Stack.Item>
|
<Stack.Item>
|
||||||
<PrimaryButton onClick={exportAll} text={intl.get("app.backup")} />
|
<PrimaryButton
|
||||||
|
onClick={exportAll}
|
||||||
|
text={intl.get("app.backup")}
|
||||||
|
/>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<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.Item>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,8 +2,25 @@ import * as React from "react"
|
|||||||
import intl from "react-intl-universal"
|
import intl from "react-intl-universal"
|
||||||
import { SourceGroup } from "../../schema-types"
|
import { SourceGroup } from "../../schema-types"
|
||||||
import { SourceState, RSSSource } from "../../scripts/models/source"
|
import { SourceState, RSSSource } from "../../scripts/models/source"
|
||||||
import { IColumn, Selection, SelectionMode, DetailsList, Label, Stack,
|
import {
|
||||||
TextField, PrimaryButton, DefaultButton, Dropdown, IDropdownOption, CommandBarButton, MarqueeSelection, IDragDropEvents, MessageBar, MessageBarType, MessageBarButton } from "@fluentui/react"
|
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"
|
import DangerButton from "../utils/danger-button"
|
||||||
|
|
||||||
type GroupsTabProps = {
|
type GroupsTabProps = {
|
||||||
@ -20,10 +37,10 @@ type GroupsTabProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GroupsTabState = {
|
type GroupsTabState = {
|
||||||
[formName: string]: any,
|
[formName: string]: any
|
||||||
selectedGroup: SourceGroup,
|
selectedGroup: SourceGroup
|
||||||
selectedSources: RSSSource[],
|
selectedSources: RSSSource[]
|
||||||
dropdownIndex: number,
|
dropdownIndex: number
|
||||||
manageGroup: boolean
|
manageGroup: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,30 +62,32 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
|
|||||||
selectedGroup: null,
|
selectedGroup: null,
|
||||||
selectedSources: null,
|
selectedSources: null,
|
||||||
dropdownIndex: null,
|
dropdownIndex: null,
|
||||||
manageGroup: false
|
manageGroup: false,
|
||||||
}
|
}
|
||||||
this.groupDragDropEvents = this.getGroupDragDropEvents()
|
this.groupDragDropEvents = this.getGroupDragDropEvents()
|
||||||
this.sourcesDragDropEvents = this.getSourcesDragDropEvents()
|
this.sourcesDragDropEvents = this.getSourcesDragDropEvents()
|
||||||
this.groupSelection = new Selection({
|
this.groupSelection = new Selection({
|
||||||
getKey: g => (g as SourceGroup).index,
|
getKey: g => (g as SourceGroup).index,
|
||||||
onSelectionChanged: () => {
|
onSelectionChanged: () => {
|
||||||
let g = this.groupSelection.getSelectedCount()
|
let g = this.groupSelection.getSelectedCount()
|
||||||
? this.groupSelection.getSelection()[0] as SourceGroup : null
|
? (this.groupSelection.getSelection()[0] as SourceGroup)
|
||||||
|
: null
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedGroup: g,
|
selectedGroup: g,
|
||||||
editGroupName: g && g.isMultiple ? g.name : ""
|
editGroupName: g && g.isMultiple ? g.name : "",
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
this.sourcesSelection = new Selection({
|
this.sourcesSelection = new Selection({
|
||||||
getKey: s => (s as RSSSource).sid,
|
getKey: s => (s as RSSSource).sid,
|
||||||
onSelectionChanged: () => {
|
onSelectionChanged: () => {
|
||||||
let sources = this.sourcesSelection.getSelectedCount()
|
let sources = this.sourcesSelection.getSelectedCount()
|
||||||
? this.sourcesSelection.getSelection() as RSSSource[] : null
|
? (this.sourcesSelection.getSelection() as RSSSource[])
|
||||||
|
: null
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedSources: sources
|
selectedSources: sources,
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,9 +98,13 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
|
|||||||
minWidth: 40,
|
minWidth: 40,
|
||||||
maxWidth: 40,
|
maxWidth: 40,
|
||||||
data: "string",
|
data: "string",
|
||||||
onRender: (g: SourceGroup) => <>
|
onRender: (g: SourceGroup) => (
|
||||||
{g.isMultiple ? intl.get("groups.group") : intl.get("groups.source")}
|
<>
|
||||||
</>
|
{g.isMultiple
|
||||||
|
? intl.get("groups.group")
|
||||||
|
: intl.get("groups.source")}
|
||||||
|
</>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "capacity",
|
key: "capacity",
|
||||||
@ -89,9 +112,9 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
|
|||||||
minWidth: 40,
|
minWidth: 40,
|
||||||
maxWidth: 60,
|
maxWidth: 60,
|
||||||
data: "string",
|
data: "string",
|
||||||
onRender: (g: SourceGroup) => <>
|
onRender: (g: SourceGroup) => (
|
||||||
{g.isMultiple ? g.sids.length : ""}
|
<>{g.isMultiple ? g.sids.length : ""}</>
|
||||||
</>
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "name",
|
key: "name",
|
||||||
@ -99,10 +122,12 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
|
|||||||
minWidth: 200,
|
minWidth: 200,
|
||||||
data: "string",
|
data: "string",
|
||||||
isRowHeader: true,
|
isRowHeader: true,
|
||||||
onRender: (g: SourceGroup) => <>
|
onRender: (g: SourceGroup) => (
|
||||||
{g.isMultiple ? g.name : this.props.sources[g.sids[0]].name}
|
<>
|
||||||
</>
|
{g.isMultiple ? g.name : this.props.sources[g.sids[0]].name}
|
||||||
}
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
sourceColumns: IColumn[] = [
|
sourceColumns: IColumn[] = [
|
||||||
@ -114,25 +139,24 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
|
|||||||
iconName: "ImagePixel",
|
iconName: "ImagePixel",
|
||||||
minWidth: 16,
|
minWidth: 16,
|
||||||
maxWidth: 16,
|
maxWidth: 16,
|
||||||
onRender: (s: RSSSource) => s.iconurl && (
|
onRender: (s: RSSSource) =>
|
||||||
<img src={s.iconurl} className="favicon" />
|
s.iconurl && <img src={s.iconurl} className="favicon" />,
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "name",
|
key: "name",
|
||||||
name: intl.get("name"),
|
name: intl.get("name"),
|
||||||
fieldName: "name",
|
fieldName: "name",
|
||||||
minWidth: 200,
|
minWidth: 200,
|
||||||
data: 'string',
|
data: "string",
|
||||||
isRowHeader: true
|
isRowHeader: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "url",
|
key: "url",
|
||||||
name: "URL",
|
name: "URL",
|
||||||
fieldName: "url",
|
fieldName: "url",
|
||||||
minWidth: 280,
|
minWidth: 280,
|
||||||
data: 'string'
|
data: "string",
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
getGroupDragDropEvents = (): IDragDropEvents => ({
|
getGroupDragDropEvents = (): IDragDropEvents => ({
|
||||||
@ -154,13 +178,15 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
|
|||||||
})
|
})
|
||||||
|
|
||||||
reorderGroups = (item: SourceGroup) => {
|
reorderGroups = (item: SourceGroup) => {
|
||||||
let draggedItem = this.groupSelection.isIndexSelected(this.groupDraggedIndex)
|
let draggedItem = this.groupSelection.isIndexSelected(
|
||||||
? this.groupSelection.getSelection()[0] as SourceGroup
|
this.groupDraggedIndex
|
||||||
: this.groupDraggedItem!
|
)
|
||||||
|
? (this.groupSelection.getSelection()[0] as SourceGroup)
|
||||||
|
: this.groupDraggedItem!
|
||||||
|
|
||||||
let insertIndex = item.index
|
let insertIndex = item.index
|
||||||
let groups = this.props.groups.filter(g => g.index != draggedItem.index)
|
let groups = this.props.groups.filter(g => g.index != draggedItem.index)
|
||||||
|
|
||||||
groups.splice(insertIndex, 0, draggedItem)
|
groups.splice(insertIndex, 0, draggedItem)
|
||||||
this.groupSelection.setAllSelected(false)
|
this.groupSelection.setAllSelected(false)
|
||||||
this.props.reorderGroups(groups)
|
this.props.reorderGroups(groups)
|
||||||
@ -185,15 +211,21 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
|
|||||||
})
|
})
|
||||||
|
|
||||||
reorderSources = (item: RSSSource) => {
|
reorderSources = (item: RSSSource) => {
|
||||||
let draggedItems = this.sourcesSelection.isIndexSelected(this.sourcesDraggedIndex)
|
let draggedItems = this.sourcesSelection.isIndexSelected(
|
||||||
? (this.sourcesSelection.getSelection() as RSSSource[]).map(s => s.sid)
|
this.sourcesDraggedIndex
|
||||||
: [this.sourcesDraggedItem!.sid]
|
)
|
||||||
|
? (this.sourcesSelection.getSelection() as RSSSource[]).map(
|
||||||
|
s => s.sid
|
||||||
|
)
|
||||||
|
: [this.sourcesDraggedItem!.sid]
|
||||||
|
|
||||||
let insertIndex = this.state.selectedGroup.sids.indexOf(item.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)
|
items.splice(insertIndex, 0, ...draggedItems)
|
||||||
|
|
||||||
let group = { ...this.state.selectedGroup, sids: items }
|
let group = { ...this.state.selectedGroup, sids: items }
|
||||||
this.props.updateGroup(group)
|
this.props.updateGroup(group)
|
||||||
this.setState({ selectedGroup: group })
|
this.setState({ selectedGroup: group })
|
||||||
@ -204,19 +236,22 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
|
|||||||
this.setState({
|
this.setState({
|
||||||
selectedGroup: g,
|
selectedGroup: g,
|
||||||
editGroupName: g && g.isMultiple ? g.name : "",
|
editGroupName: g && g.isMultiple ? g.name : "",
|
||||||
manageGroup: true
|
manageGroup: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dropdownOptions = () => this.props.groups.filter(g => g.isMultiple).map(g => ({
|
dropdownOptions = () =>
|
||||||
key: g.index,
|
this.props.groups
|
||||||
text: g.name
|
.filter(g => g.isMultiple)
|
||||||
}))
|
.map(g => ({
|
||||||
|
key: g.index,
|
||||||
|
text: g.name,
|
||||||
|
}))
|
||||||
|
|
||||||
handleInputChange = (event) => {
|
handleInputChange = event => {
|
||||||
const name: string = event.target.name
|
const name: string = event.target.name
|
||||||
this.setState({[name]: event.target.value})
|
this.setState({ [name]: event.target.value })
|
||||||
}
|
}
|
||||||
|
|
||||||
validateNewGroupName = (v: string) => {
|
validateNewGroupName = (v: string) => {
|
||||||
@ -235,21 +270,32 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
|
|||||||
createGroup = (event: React.FormEvent) => {
|
createGroup = (event: React.FormEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
let trimmed = this.state.newGroupName.trim()
|
let trimmed = this.state.newGroupName.trim()
|
||||||
if (this.validateNewGroupName(trimmed) === "") this.props.createGroup(trimmed)
|
if (this.validateNewGroupName(trimmed) === "")
|
||||||
|
this.props.createGroup(trimmed)
|
||||||
}
|
}
|
||||||
|
|
||||||
addToGroup = () => {
|
addToGroup = () => {
|
||||||
this.props.addToGroup(this.state.dropdownIndex, this.state.selectedGroup.sids[0])
|
this.props.addToGroup(
|
||||||
|
this.state.dropdownIndex,
|
||||||
|
this.state.selectedGroup.sids[0]
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
removeFromGroup = () => {
|
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 })
|
this.setState({ selectedSources: null })
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteGroup = () => {
|
deleteGroup = () => {
|
||||||
this.props.deleteGroup(this.state.selectedGroup.index)
|
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 })
|
this.setState({ selectedGroup: null })
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -265,126 +311,194 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
|
|||||||
|
|
||||||
render = () => (
|
render = () => (
|
||||||
<div className="tab-body">
|
<div className="tab-body">
|
||||||
{this.state.manageGroup && this.state.selectedGroup &&
|
{this.state.manageGroup && this.state.selectedGroup && (
|
||||||
<>
|
<>
|
||||||
<Stack horizontal horizontalAlign="space-between" style={{height: 40}}>
|
<Stack
|
||||||
<CommandBarButton
|
horizontal
|
||||||
text={intl.get("groups.exitGroup")}
|
horizontalAlign="space-between"
|
||||||
iconProps={{iconName: "BackToWindow"}}
|
style={{ height: 40 }}>
|
||||||
onClick={() => this.setState({manageGroup: false})} />
|
<CommandBarButton
|
||||||
{this.state.selectedSources != null && <CommandBarButton
|
text={intl.get("groups.exitGroup")}
|
||||||
text={intl.get("groups.deleteSource")}
|
iconProps={{ iconName: "BackToWindow" }}
|
||||||
onClick={this.removeFromGroup}
|
onClick={() =>
|
||||||
iconProps={{iconName: "RemoveFromShoppingList", style: {color: "#d13438"}}} />}
|
this.setState({ manageGroup: false })
|
||||||
</Stack>
|
}
|
||||||
|
/>
|
||||||
<MarqueeSelection selection={this.sourcesSelection} isDraggingConstrainedToRoot={true}>
|
{this.state.selectedSources != null && (
|
||||||
<DetailsList
|
<CommandBarButton
|
||||||
compact={true}
|
text={intl.get("groups.deleteSource")}
|
||||||
items={this.state.selectedGroup.sids.map(sid => this.props.sources[sid])}
|
onClick={this.removeFromGroup}
|
||||||
columns={this.sourceColumns}
|
iconProps={{
|
||||||
dragDropEvents={this.sourcesDragDropEvents}
|
iconName: "RemoveFromShoppingList",
|
||||||
setKey="multiple"
|
style: { color: "#d13438" },
|
||||||
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>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
|
||||||
|
|
||||||
<DetailsList
|
<MarqueeSelection
|
||||||
compact={true}
|
selection={this.sourcesSelection}
|
||||||
items={this.props.groups}
|
isDraggingConstrainedToRoot={true}>
|
||||||
columns={this.groupColumns()}
|
<DetailsList
|
||||||
setKey="selected"
|
compact={true}
|
||||||
onItemInvoked={this.manageGroup}
|
items={this.state.selectedGroup.sids.map(
|
||||||
dragDropEvents={this.groupDragDropEvents}
|
sid => this.props.sources[sid]
|
||||||
selection={this.groupSelection}
|
)}
|
||||||
selectionMode={SelectionMode.single} />
|
columns={this.sourceColumns}
|
||||||
|
dragDropEvents={this.sourcesDragDropEvents}
|
||||||
|
setKey="multiple"
|
||||||
|
selection={this.sourcesSelection}
|
||||||
|
selectionMode={SelectionMode.multiple}
|
||||||
|
/>
|
||||||
|
</MarqueeSelection>
|
||||||
|
|
||||||
{this.state.selectedGroup
|
<span className="settings-hint">
|
||||||
? ( this.state.selectedGroup.isMultiple
|
{intl.get("groups.sourceHint")}
|
||||||
?<>
|
</span>
|
||||||
<Label>{intl.get("groups.selectedGroup")}</Label>
|
</>
|
||||||
|
)}
|
||||||
|
{!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 horizontal>
|
||||||
<Stack.Item grow>
|
<Stack.Item grow>
|
||||||
<TextField
|
<TextField
|
||||||
onGetErrorMessage={v => v.trim().length == 0 ? intl.get("emptyName") : ""}
|
onGetErrorMessage={
|
||||||
|
this.validateNewGroupName
|
||||||
|
}
|
||||||
validateOnLoad={false}
|
validateOnLoad={false}
|
||||||
placeholder={intl.get("groups.enterName")}
|
placeholder={intl.get("groups.enterName")}
|
||||||
value={this.state.editGroupName}
|
value={this.state.newGroupName}
|
||||||
name="editGroupName"
|
id="newGroupName"
|
||||||
onChange={this.handleInputChange} />
|
name="newGroupName"
|
||||||
|
onChange={this.handleInputChange}
|
||||||
|
/>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<Stack.Item>
|
<Stack.Item>
|
||||||
<DefaultButton
|
<PrimaryButton
|
||||||
disabled={this.state.editGroupName.trim().length == 0}
|
disabled={
|
||||||
onClick={this.updateGroupName}
|
this.validateNewGroupName(
|
||||||
text={intl.get("groups.editName")} />
|
this.state.newGroupName
|
||||||
</Stack.Item>
|
) !== ""
|
||||||
<Stack.Item>
|
}
|
||||||
<DangerButton
|
type="sumbit"
|
||||||
key={this.state.selectedGroup.index}
|
text={intl.get("create")}
|
||||||
onClick={this.deleteGroup}
|
/>
|
||||||
text={intl.get("groups.deleteGroup")} />
|
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
</Stack>
|
</Stack>
|
||||||
</>
|
</form>
|
||||||
:<>
|
|
||||||
<Label>{intl.get("groups.selectedSource")}</Label>
|
<DetailsList
|
||||||
<Stack horizontal>
|
compact={true}
|
||||||
<Stack.Item grow>
|
items={this.props.groups}
|
||||||
<Dropdown
|
columns={this.groupColumns()}
|
||||||
placeholder={intl.get("groups.chooseGroup")}
|
setKey="selected"
|
||||||
selectedKey={this.state.dropdownIndex}
|
onItemInvoked={this.manageGroup}
|
||||||
options={this.dropdownOptions()}
|
dragDropEvents={this.groupDragDropEvents}
|
||||||
onChange={this.dropdownChange} />
|
selection={this.groupSelection}
|
||||||
</Stack.Item>
|
selectionMode={SelectionMode.single}
|
||||||
<Stack.Item>
|
/>
|
||||||
<DefaultButton
|
|
||||||
disabled={this.state.dropdownIndex === null}
|
{this.state.selectedGroup ? (
|
||||||
onClick={this.addToGroup}
|
this.state.selectedGroup.isMultiple ? (
|
||||||
text={intl.get("groups.addToGroup")} />
|
<>
|
||||||
</Stack.Item>
|
<Label>
|
||||||
</Stack>
|
{intl.get("groups.selectedGroup")}
|
||||||
</>
|
</Label>
|
||||||
)
|
<Stack horizontal>
|
||||||
: <span className="settings-hint">{intl.get("groups.groupHint")}</span>
|
<Stack.Item grow>
|
||||||
}
|
<TextField
|
||||||
</> : null}
|
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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GroupsTab
|
export default GroupsTab
|
||||||
|
@ -1,8 +1,27 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import intl from "react-intl-universal"
|
import intl from "react-intl-universal"
|
||||||
import { SourceState, RSSSource } from "../../scripts/models/source"
|
import { SourceState, RSSSource } from "../../scripts/models/source"
|
||||||
import { Stack, Label, Dropdown, IDropdownOption, TextField, PrimaryButton, Icon, DropdownMenuItemType,
|
import {
|
||||||
DefaultButton, DetailsList, IColumn, CommandBar, ICommandBarItemProps, Selection, SelectionMode, MarqueeSelection, IDragDropEvents, Link, IIconProps } from "@fluentui/react"
|
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 { SourceRule, RuleActions } from "../../scripts/models/rule"
|
||||||
import { FilterType } from "../../scripts/models/feed"
|
import { FilterType } from "../../scripts/models/feed"
|
||||||
import { validateRegex } from "../../scripts/utils"
|
import { validateRegex } from "../../scripts/utils"
|
||||||
@ -60,13 +79,15 @@ class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
|
|||||||
mockTitle: "",
|
mockTitle: "",
|
||||||
mockCreator: "",
|
mockCreator: "",
|
||||||
mockContent: "",
|
mockContent: "",
|
||||||
mockResult: ""
|
mockResult: "",
|
||||||
}
|
}
|
||||||
this.rulesSelection = new Selection({
|
this.rulesSelection = new Selection({
|
||||||
getKey: (_, i) => i,
|
getKey: (_, i) => i,
|
||||||
onSelectionChanged: () => {
|
onSelectionChanged: () => {
|
||||||
this.setState({selectedRules: this.rulesSelection.getSelectedIndices()})
|
this.setState({
|
||||||
}
|
selectedRules: this.rulesSelection.getSelectedIndices(),
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
this.rulesDragDropEvents = this.getRulesDragDropEvents()
|
this.rulesDragDropEvents = this.getRulesDragDropEvents()
|
||||||
}
|
}
|
||||||
@ -91,13 +112,15 @@ class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
|
|||||||
|
|
||||||
reorderRules = (item: SourceRule) => {
|
reorderRules = (item: SourceRule) => {
|
||||||
let rules = this.getSourceRules()
|
let rules = this.getSourceRules()
|
||||||
let draggedItems = this.rulesSelection.isIndexSelected(this.rulesDraggedIndex)
|
let draggedItems = this.rulesSelection.isIndexSelected(
|
||||||
? this.rulesSelection.getSelection() as SourceRule[]
|
this.rulesDraggedIndex
|
||||||
: [this.rulesDraggedItem]
|
)
|
||||||
|
? (this.rulesSelection.getSelection() as SourceRule[])
|
||||||
|
: [this.rulesDraggedItem]
|
||||||
|
|
||||||
let insertIndex = rules.indexOf(item)
|
let insertIndex = rules.indexOf(item)
|
||||||
let items = rules.filter(r => !draggedItems.includes(r))
|
let items = rules.filter(r => !draggedItems.includes(r))
|
||||||
|
|
||||||
items.splice(insertIndex, 0, ...draggedItems)
|
items.splice(insertIndex, 0, ...draggedItems)
|
||||||
this.rulesSelection.setAllSelected(false)
|
this.rulesSelection.setAllSelected(false)
|
||||||
let source = this.props.sources[parseInt(this.state.sid)]
|
let source = this.props.sources[parseInt(this.state.sid)]
|
||||||
@ -113,9 +136,11 @@ class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
|
|||||||
this.setState({
|
this.setState({
|
||||||
regex: rule ? rule.filter.search : "",
|
regex: rule ? rule.filter.search : "",
|
||||||
searchType: searchType,
|
searchType: searchType,
|
||||||
caseSensitive: rule ? !(rule.filter.type & FilterType.CaseInsensitive) : false,
|
caseSensitive: rule
|
||||||
|
? !(rule.filter.type & FilterType.CaseInsensitive)
|
||||||
|
: false,
|
||||||
match: rule ? rule.match : true,
|
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"),
|
name: intl.get("rules.regex"),
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
maxWidth: 200,
|
maxWidth: 200,
|
||||||
onRender: (rule: SourceRule) => rule.filter.search
|
onRender: (rule: SourceRule) => rule.filter.search,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "actions",
|
key: "actions",
|
||||||
name: intl.get("rules.action"),
|
name: intl.get("rules.action"),
|
||||||
minWidth: 100,
|
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"
|
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]) => ({
|
sourceOptions = (): IDropdownOption[] =>
|
||||||
key: sid,
|
Object.entries(this.props.sources).map(([sid, s]) => ({
|
||||||
text: s.name,
|
key: sid,
|
||||||
data: { icon: s.iconurl }
|
text: s.name,
|
||||||
}))
|
data: { icon: s.iconurl },
|
||||||
|
}))
|
||||||
onRenderSourceOption = (option: IDropdownOption) => (
|
onRenderSourceOption = (option: IDropdownOption) => (
|
||||||
<div>
|
<div>
|
||||||
{option.data && option.data.icon && (
|
{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>
|
<span>{option.text}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -162,9 +191,14 @@ class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
|
|||||||
onSourceOptionChange = (_, item: IDropdownOption) => {
|
onSourceOptionChange = (_, item: IDropdownOption) => {
|
||||||
this.initRuleEdit()
|
this.initRuleEdit()
|
||||||
this.rulesSelection.setAllSelected(false)
|
this.rulesSelection.setAllSelected(false)
|
||||||
this.setState({
|
this.setState({
|
||||||
sid: item.key as string, selectedRules: [], editIndex: -1,
|
sid: item.key as string,
|
||||||
mockTitle: "", mockCreator: "", mockContent: "", mockResult: ""
|
selectedRules: [],
|
||||||
|
editIndex: -1,
|
||||||
|
mockTitle: "",
|
||||||
|
mockCreator: "",
|
||||||
|
mockContent: "",
|
||||||
|
mockResult: "",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,38 +213,51 @@ class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
|
|||||||
|
|
||||||
matchOptions = (): IDropdownOption[] => [
|
matchOptions = (): IDropdownOption[] => [
|
||||||
{ key: 1, text: intl.get("rules.match") },
|
{ key: 1, text: intl.get("rules.match") },
|
||||||
{ key: 0, text: intl.get("rules.notMatch") }
|
{ key: 0, text: intl.get("rules.notMatch") },
|
||||||
]
|
]
|
||||||
onMatchOptionChange = (_, item: IDropdownOption) => {
|
onMatchOptionChange = (_, item: IDropdownOption) => {
|
||||||
this.setState({ match: Boolean(item.key) })
|
this.setState({ match: Boolean(item.key) })
|
||||||
}
|
}
|
||||||
|
|
||||||
actionOptions = (): IDropdownOption[] => [
|
actionOptions = (): IDropdownOption[] =>
|
||||||
...Object.entries(actionKeyMap).map(([k, t], i) => {
|
[
|
||||||
if (k.includes("-false")) {
|
...Object.entries(actionKeyMap).map(([k, t], i) => {
|
||||||
return [{ key: k, text: intl.get(t) }, { key: i, text: "-", itemType: DropdownMenuItemType.Divider }]
|
if (k.includes("-false")) {
|
||||||
} else {
|
return [
|
||||||
return [{ key: k, text: intl.get(t) }]
|
{ key: k, text: intl.get(t) },
|
||||||
}
|
{
|
||||||
})
|
key: i,
|
||||||
].flat(1)
|
text: "-",
|
||||||
|
itemType: DropdownMenuItemType.Divider,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
return [{ key: k, text: intl.get(t) }]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
].flat(1)
|
||||||
|
|
||||||
onActionOptionChange = (_, item: IDropdownOption) => {
|
onActionOptionChange = (_, item: IDropdownOption) => {
|
||||||
if (item.selected) {
|
if (item.selected) {
|
||||||
this.setState(prevState => {
|
this.setState(prevState => {
|
||||||
let [a, f] = (item.key as string).split("-")
|
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)
|
keys.push(item.key as string)
|
||||||
return { actionKeys: keys }
|
return { actionKeys: keys }
|
||||||
})
|
})
|
||||||
} else {
|
} 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) => {
|
validateRegexField = (value: string) => {
|
||||||
if (value.length === 0) return intl.get("emptyField")
|
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 ""
|
else return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,10 +265,16 @@ class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
|
|||||||
let filterType = FilterType.Default | FilterType.ShowHidden
|
let filterType = FilterType.Default | FilterType.ShowHidden
|
||||||
if (!this.state.caseSensitive) filterType |= FilterType.CaseInsensitive
|
if (!this.state.caseSensitive) filterType |= FilterType.CaseInsensitive
|
||||||
if (this.state.searchType === 1) filterType |= FilterType.FullSearch
|
if (this.state.searchType === 1) filterType |= FilterType.FullSearch
|
||||||
else if (this.state.searchType === 2) filterType |= FilterType.CreatorSearch
|
else if (this.state.searchType === 2)
|
||||||
let rule = new SourceRule(this.state.regex, this.state.actionKeys, filterType, this.state.match)
|
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 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) {
|
if (this.state.editIndex === -1) {
|
||||||
rules.push(rule)
|
rules.push(rule)
|
||||||
} else {
|
} else {
|
||||||
@ -243,28 +296,39 @@ class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
|
|||||||
let rules = this.getSourceRules()
|
let rules = this.getSourceRules()
|
||||||
for (let i of this.state.selectedRules) rules[i] = null
|
for (let i of this.state.selectedRules) rules[i] = null
|
||||||
let source = this.props.sources[parseInt(this.state.sid)]
|
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()
|
this.initRuleEdit()
|
||||||
}
|
}
|
||||||
|
|
||||||
commandBarItems = (): ICommandBarItemProps[] => [{
|
commandBarItems = (): ICommandBarItemProps[] => [
|
||||||
key: "new", text: intl.get("rules.new"), iconProps: { iconName: "Add" },
|
{
|
||||||
onClick: this.newRule
|
key: "new",
|
||||||
}]
|
text: intl.get("rules.new"),
|
||||||
|
iconProps: { iconName: "Add" },
|
||||||
|
onClick: this.newRule,
|
||||||
|
},
|
||||||
|
]
|
||||||
commandBarFarItems = (): ICommandBarItemProps[] => {
|
commandBarFarItems = (): ICommandBarItemProps[] => {
|
||||||
let items = []
|
let items = []
|
||||||
if (this.state.selectedRules.length === 1) {
|
if (this.state.selectedRules.length === 1) {
|
||||||
let index = this.state.selectedRules[0]
|
let index = this.state.selectedRules[0]
|
||||||
items.push({
|
items.push({
|
||||||
key: "edit", text: intl.get("edit"), iconProps: { iconName: "Edit" },
|
key: "edit",
|
||||||
onClick: () => this.editRule(this.getSourceRules()[index], index)
|
text: intl.get("edit"),
|
||||||
|
iconProps: { iconName: "Edit" },
|
||||||
|
onClick: () =>
|
||||||
|
this.editRule(this.getSourceRules()[index], index),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (this.state.selectedRules.length > 0) {
|
if (this.state.selectedRules.length > 0) {
|
||||||
items.push({
|
items.push({
|
||||||
key: "del", text: intl.get("delete"),
|
key: "del",
|
||||||
|
text: intl.get("delete"),
|
||||||
iconProps: { iconName: "Delete", style: { color: "#d13438" } },
|
iconProps: { iconName: "Delete", style: { color: "#d13438" } },
|
||||||
onClick: this.deleteRules
|
onClick: this.deleteRules,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return items
|
return items
|
||||||
@ -278,7 +342,9 @@ class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
|
|||||||
item.creator = this.state.mockCreator
|
item.creator = this.state.mockCreator
|
||||||
SourceRule.applyAll(this.getSourceRules(), item)
|
SourceRule.applyAll(this.getSourceRules(), item)
|
||||||
let result = []
|
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.starred) result.push(intl.get("article.star"))
|
||||||
if (item.hidden) result.push(intl.get("article.hide"))
|
if (item.hidden) result.push(intl.get("article.hide"))
|
||||||
if (item.notify) result.push(intl.get("article.notify"))
|
if (item.notify) result.push(intl.get("article.notify"))
|
||||||
@ -291,20 +357,22 @@ class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
|
|||||||
regexCaseIconProps = (): IIconProps => ({
|
regexCaseIconProps = (): IIconProps => ({
|
||||||
title: intl.get("context.caseSensitive"),
|
title: intl.get("context.caseSensitive"),
|
||||||
children: "Aa",
|
children: "Aa",
|
||||||
style: {
|
style: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontStyle: "normal",
|
fontStyle: "normal",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
pointerEvents: "unset",
|
pointerEvents: "unset",
|
||||||
color: this.state.caseSensitive ? "var(--black)" : "var(--neutralTertiary)",
|
color: this.state.caseSensitive
|
||||||
|
? "var(--black)"
|
||||||
|
: "var(--neutralTertiary)",
|
||||||
textDecoration: this.state.caseSensitive ? "underline" : "",
|
textDecoration: this.state.caseSensitive ? "underline" : "",
|
||||||
},
|
},
|
||||||
onClick: this.toggleCaseSensitivity
|
onClick: this.toggleCaseSensitivity,
|
||||||
})
|
})
|
||||||
|
|
||||||
render = () => (
|
render = () => (
|
||||||
<div className="tab-body">
|
<div className="tab-body">
|
||||||
<Stack horizontal tokens={{childrenGap: 16}}>
|
<Stack horizontal tokens={{ childrenGap: 16 }}>
|
||||||
<Stack.Item>
|
<Stack.Item>
|
||||||
<Label>{intl.get("rules.source")}</Label>
|
<Label>{intl.get("rules.source")}</Label>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
@ -315,124 +383,169 @@ class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
|
|||||||
onRenderOption={this.onRenderSourceOption}
|
onRenderOption={this.onRenderSourceOption}
|
||||||
onRenderTitle={this.onRenderSourceTitle}
|
onRenderTitle={this.onRenderSourceTitle}
|
||||||
selectedKey={this.state.sid}
|
selectedKey={this.state.sid}
|
||||||
onChange={this.onSourceOptionChange} />
|
onChange={this.onSourceOptionChange}
|
||||||
|
/>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{this.state.sid
|
{this.state.sid ? (
|
||||||
? (this.state.editIndex > -1 || !this.getSourceRules() || this.getSourceRules().length === 0
|
this.state.editIndex > -1 ||
|
||||||
? <>
|
!this.getSourceRules() ||
|
||||||
<Label>
|
this.getSourceRules().length === 0 ? (
|
||||||
{intl.get((this.state.editIndex >= 0 && this.state.editIndex < this.getSourceRules().length) ? "edit" : "rules.new")}
|
<>
|
||||||
</Label>
|
<Label>
|
||||||
<Stack horizontal>
|
{intl.get(
|
||||||
<Stack.Item>
|
this.state.editIndex >= 0 &&
|
||||||
<Label>{intl.get("rules.if")}</Label>
|
this.state.editIndex <
|
||||||
</Stack.Item>
|
this.getSourceRules().length
|
||||||
<Stack.Item>
|
? "edit"
|
||||||
<Dropdown
|
: "rules.new"
|
||||||
options={this.searchOptions()}
|
)}
|
||||||
selectedKey={this.state.searchType}
|
</Label>
|
||||||
onChange={this.onSearchOptionChange}
|
<Stack horizontal>
|
||||||
style={{width: 140}} />
|
<Stack.Item>
|
||||||
</Stack.Item>
|
<Label>{intl.get("rules.if")}</Label>
|
||||||
<Stack.Item>
|
</Stack.Item>
|
||||||
<Dropdown
|
<Stack.Item>
|
||||||
options={this.matchOptions()}
|
<Dropdown
|
||||||
selectedKey={this.state.match ? 1 : 0}
|
options={this.searchOptions()}
|
||||||
onChange={this.onMatchOptionChange}
|
selectedKey={this.state.searchType}
|
||||||
style={{width: 130}} />
|
onChange={this.onSearchOptionChange}
|
||||||
</Stack.Item>
|
style={{ width: 140 }}
|
||||||
<Stack.Item grow>
|
/>
|
||||||
<TextField
|
</Stack.Item>
|
||||||
name="regex"
|
<Stack.Item>
|
||||||
placeholder={intl.get("rules.regex")}
|
<Dropdown
|
||||||
iconProps={this.regexCaseIconProps()}
|
options={this.matchOptions()}
|
||||||
value={this.state.regex}
|
selectedKey={this.state.match ? 1 : 0}
|
||||||
onGetErrorMessage={this.validateRegexField}
|
onChange={this.onMatchOptionChange}
|
||||||
validateOnLoad={false}
|
style={{ width: 130 }}
|
||||||
onChange={this.handleInputChange} />
|
/>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
</Stack>
|
<Stack.Item grow>
|
||||||
<Stack horizontal>
|
<TextField
|
||||||
<Stack.Item>
|
name="regex"
|
||||||
<Label>{intl.get("rules.then")}</Label>
|
placeholder={intl.get("rules.regex")}
|
||||||
</Stack.Item>
|
iconProps={this.regexCaseIconProps()}
|
||||||
<Stack.Item grow>
|
value={this.state.regex}
|
||||||
<Dropdown multiSelect
|
onGetErrorMessage={this.validateRegexField}
|
||||||
placeholder={intl.get("rules.selectAction")}
|
validateOnLoad={false}
|
||||||
options={this.actionOptions()}
|
onChange={this.handleInputChange}
|
||||||
selectedKeys={this.state.actionKeys}
|
/>
|
||||||
onChange={this.onActionOptionChange}
|
</Stack.Item>
|
||||||
onRenderCaretDown={() => <Icon iconName="CirclePlus" />} />
|
</Stack>
|
||||||
</Stack.Item>
|
<Stack horizontal>
|
||||||
</Stack>
|
<Stack.Item>
|
||||||
<Stack horizontal>
|
<Label>{intl.get("rules.then")}</Label>
|
||||||
<Stack.Item>
|
</Stack.Item>
|
||||||
<PrimaryButton
|
<Stack.Item grow>
|
||||||
disabled={this.state.regex.length == 0 || validateRegex(this.state.regex) === null || this.state.actionKeys.length == 0}
|
<Dropdown
|
||||||
text={intl.get("confirm")}
|
multiSelect
|
||||||
onClick={this.saveRule} />
|
placeholder={intl.get("rules.selectAction")}
|
||||||
</Stack.Item>
|
options={this.actionOptions()}
|
||||||
{this.state.editIndex > -1 && <Stack.Item>
|
selectedKeys={this.state.actionKeys}
|
||||||
<DefaultButton
|
onChange={this.onActionOptionChange}
|
||||||
text={intl.get("cancel")}
|
onRenderCaretDown={() => (
|
||||||
onClick={() => this.setState({ editIndex: -1 })} />
|
<Icon iconName="CirclePlus" />
|
||||||
</Stack.Item>}
|
)}
|
||||||
</Stack>
|
/>
|
||||||
</>
|
</Stack.Item>
|
||||||
: <>
|
</Stack>
|
||||||
<CommandBar
|
<Stack horizontal>
|
||||||
items={this.commandBarItems()}
|
<Stack.Item>
|
||||||
farItems={this.commandBarFarItems()} />
|
<PrimaryButton
|
||||||
<MarqueeSelection selection={this.rulesSelection} isDraggingConstrainedToRoot>
|
disabled={
|
||||||
<DetailsList compact
|
this.state.regex.length == 0 ||
|
||||||
columns={this.ruleColumns()}
|
validateRegex(this.state.regex) ===
|
||||||
items={this.getSourceRules()}
|
null ||
|
||||||
onItemInvoked={this.editRule}
|
this.state.actionKeys.length == 0
|
||||||
dragDropEvents={this.rulesDragDropEvents}
|
}
|
||||||
setKey="selected"
|
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}
|
selection={this.rulesSelection}
|
||||||
selectionMode={SelectionMode.multiple} />
|
isDraggingConstrainedToRoot>
|
||||||
</MarqueeSelection>
|
<DetailsList
|
||||||
<span className="settings-hint up">{intl.get("rules.hint")}</span>
|
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>
|
<Label>{intl.get("rules.test")}</Label>
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<Stack.Item grow>
|
<Stack.Item grow>
|
||||||
<TextField
|
<TextField
|
||||||
name="mockTitle"
|
name="mockTitle"
|
||||||
placeholder={intl.get("rules.title")}
|
placeholder={intl.get("rules.title")}
|
||||||
value={this.state.mockTitle}
|
value={this.state.mockTitle}
|
||||||
onChange={this.handleInputChange} />
|
onChange={this.handleInputChange}
|
||||||
</Stack.Item>
|
/>
|
||||||
<Stack.Item grow>
|
</Stack.Item>
|
||||||
<TextField
|
<Stack.Item grow>
|
||||||
name="mockCreator"
|
<TextField
|
||||||
placeholder={intl.get("rules.creator")}
|
name="mockCreator"
|
||||||
value={this.state.mockCreator}
|
placeholder={intl.get("rules.creator")}
|
||||||
onChange={this.handleInputChange} />
|
value={this.state.mockCreator}
|
||||||
</Stack.Item>
|
onChange={this.handleInputChange}
|
||||||
</Stack>
|
/>
|
||||||
<Stack horizontal>
|
</Stack.Item>
|
||||||
<Stack.Item grow>
|
</Stack>
|
||||||
<TextField
|
<Stack horizontal>
|
||||||
name="mockContent"
|
<Stack.Item grow>
|
||||||
placeholder={intl.get("rules.content")}
|
<TextField
|
||||||
value={this.state.mockContent}
|
name="mockContent"
|
||||||
onChange={this.handleInputChange} />
|
placeholder={intl.get("rules.content")}
|
||||||
</Stack.Item>
|
value={this.state.mockContent}
|
||||||
<Stack.Item>
|
onChange={this.handleInputChange}
|
||||||
<PrimaryButton
|
/>
|
||||||
text={intl.get("confirm")}
|
</Stack.Item>
|
||||||
onClick={this.testMockItem} />
|
<Stack.Item>
|
||||||
</Stack.Item>
|
<PrimaryButton
|
||||||
</Stack>
|
text={intl.get("confirm")}
|
||||||
<span className="settings-hint up">{this.state.mockResult}</span>
|
onClick={this.testMockItem}
|
||||||
</>)
|
/>
|
||||||
: (
|
</Stack.Item>
|
||||||
<Stack horizontalAlign="center" style={{marginTop: 64}}>
|
</Stack>
|
||||||
<Stack className="settings-rules-icons" horizontal tokens={{childrenGap: 12}}>
|
<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="Filter" />
|
||||||
<Icon iconName="FavoriteStar" />
|
<Icon iconName="FavoriteStar" />
|
||||||
<Icon iconName="Ringer" />
|
<Icon iconName="Ringer" />
|
||||||
@ -440,9 +553,13 @@ class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
|
|||||||
</Stack>
|
</Stack>
|
||||||
<span className="settings-hint">
|
<span className="settings-hint">
|
||||||
{intl.get("rules.intro")}
|
{intl.get("rules.intro")}
|
||||||
<Link
|
<Link
|
||||||
onClick={() => window.utils.openExternal("https://github.com/yang991178/fluent-reader/wiki/Support#rules")}
|
onClick={() =>
|
||||||
style={{marginLeft: 6}}>
|
window.utils.openExternal(
|
||||||
|
"https://github.com/yang991178/fluent-reader/wiki/Support#rules"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
style={{ marginLeft: 6 }}>
|
||||||
{intl.get("rules.help")}
|
{intl.get("rules.help")}
|
||||||
</Link>
|
</Link>
|
||||||
</span>
|
</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
|
type: SyncService
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ServiceTab extends React.Component<ServiceTabProps, ServiceTabState> {
|
export class ServiceTab extends React.Component<
|
||||||
|
ServiceTabProps,
|
||||||
|
ServiceTabState
|
||||||
|
> {
|
||||||
constructor(props: ServiceTabProps) {
|
constructor(props: ServiceTabProps) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = {
|
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) => {
|
onServiceOptionChange = (_, option: IDropdownOption) => {
|
||||||
if (option.key === -1) {
|
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 {
|
} else {
|
||||||
this.setState({ type: option.key as number })
|
this.setState({ type: option.key as number })
|
||||||
}
|
}
|
||||||
@ -55,29 +60,60 @@ export class ServiceTab extends React.Component<ServiceTabProps, ServiceTabState
|
|||||||
|
|
||||||
getConfigsTab = () => {
|
getConfigsTab = () => {
|
||||||
switch (this.state.type) {
|
switch (this.state.type) {
|
||||||
case SyncService.Fever: return <FeverConfigsTab {...this.props} exit={this.exitConfigsTab} />
|
case SyncService.Fever:
|
||||||
case SyncService.Feedbin: return <FeedbinConfigsTab {...this.props} exit={this.exitConfigsTab} />
|
return (
|
||||||
case SyncService.GReader: return <GReaderConfigsTab {...this.props} exit={this.exitConfigsTab} />
|
<FeverConfigsTab
|
||||||
case SyncService.Inoreader: return <InoreaderConfigsTab {...this.props} exit={this.exitConfigsTab} />
|
{...this.props}
|
||||||
default: return null
|
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 = () => (
|
render = () => (
|
||||||
<div className="tab-body">
|
<div className="tab-body">
|
||||||
{this.state.type === SyncService.None
|
{this.state.type === SyncService.None ? (
|
||||||
? (
|
<Stack horizontalAlign="center" style={{ marginTop: 64 }}>
|
||||||
<Stack horizontalAlign="center" style={{marginTop: 64}}>
|
<Stack
|
||||||
<Stack className="settings-rules-icons" horizontal tokens={{childrenGap: 12}}>
|
className="settings-rules-icons"
|
||||||
|
horizontal
|
||||||
|
tokens={{ childrenGap: 12 }}>
|
||||||
<Icon iconName="ThisPC" />
|
<Icon iconName="ThisPC" />
|
||||||
<Icon iconName="Sync" />
|
<Icon iconName="Sync" />
|
||||||
<Icon iconName="Cloud" />
|
<Icon iconName="Cloud" />
|
||||||
</Stack>
|
</Stack>
|
||||||
<span className="settings-hint">
|
<span className="settings-hint">
|
||||||
{intl.get("service.intro")}
|
{intl.get("service.intro")}
|
||||||
<Link
|
<Link
|
||||||
onClick={() => window.utils.openExternal("https://github.com/yang991178/fluent-reader/wiki/Support#services")}
|
onClick={() =>
|
||||||
style={{marginLeft: 6}}>
|
window.utils.openExternal(
|
||||||
|
"https://github.com/yang991178/fluent-reader/wiki/Support#services"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
style={{ marginLeft: 6 }}>
|
||||||
{intl.get("rules.help")}
|
{intl.get("rules.help")}
|
||||||
</Link>
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
@ -86,10 +122,12 @@ export class ServiceTab extends React.Component<ServiceTabProps, ServiceTabState
|
|||||||
options={this.serviceOptions()}
|
options={this.serviceOptions()}
|
||||||
selectedKey={null}
|
selectedKey={null}
|
||||||
onChange={this.onServiceOptionChange}
|
onChange={this.onServiceOptionChange}
|
||||||
style={{marginTop: 32, width: 180}} />
|
style={{ marginTop: 32, width: 180 }}
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
) : (
|
||||||
: this.getConfigsTab()}
|
this.getConfigsTab()
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,19 @@ import intl from "react-intl-universal"
|
|||||||
import { ServiceConfigsTabProps } from "../service"
|
import { ServiceConfigsTabProps } from "../service"
|
||||||
import { FeedbinConfigs } from "../../../scripts/models/services/feedbin"
|
import { FeedbinConfigs } from "../../../scripts/models/services/feedbin"
|
||||||
import { SyncService } from "../../../schema-types"
|
import { SyncService } from "../../../schema-types"
|
||||||
import { Stack, Icon, Label, TextField, PrimaryButton, DefaultButton, Checkbox,
|
import {
|
||||||
MessageBar, MessageBarType, Dropdown, IDropdownOption } from "@fluentui/react"
|
Stack,
|
||||||
|
Icon,
|
||||||
|
Label,
|
||||||
|
TextField,
|
||||||
|
PrimaryButton,
|
||||||
|
DefaultButton,
|
||||||
|
Checkbox,
|
||||||
|
MessageBar,
|
||||||
|
MessageBarType,
|
||||||
|
Dropdown,
|
||||||
|
IDropdownOption,
|
||||||
|
} from "@fluentui/react"
|
||||||
import DangerButton from "../../utils/danger-button"
|
import DangerButton from "../../utils/danger-button"
|
||||||
import { urlTest } from "../../../scripts/utils"
|
import { urlTest } from "../../../scripts/utils"
|
||||||
import LiteExporter from "./lite-exporter"
|
import LiteExporter from "./lite-exporter"
|
||||||
@ -18,7 +29,10 @@ type FeedbinConfigsTabState = {
|
|||||||
importGroups: boolean
|
importGroups: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
class FeedbinConfigsTab extends React.Component<ServiceConfigsTabProps, FeedbinConfigsTabState> {
|
class FeedbinConfigsTab extends React.Component<
|
||||||
|
ServiceConfigsTabProps,
|
||||||
|
FeedbinConfigsTabState
|
||||||
|
> {
|
||||||
constructor(props: ServiceConfigsTabProps) {
|
constructor(props: ServiceConfigsTabProps) {
|
||||||
super(props)
|
super(props)
|
||||||
const configs = props.configs as FeedbinConfigs
|
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: 750, text: intl.get("service.fetchLimitNum", { count: 750 }) },
|
||||||
{ key: 1000, text: intl.get("service.fetchLimitNum", { count: 1000 }) },
|
{ key: 1000, text: intl.get("service.fetchLimitNum", { count: 1000 }) },
|
||||||
{ key: 1500, text: intl.get("service.fetchLimitNum", { count: 1500 }) },
|
{ 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) => {
|
onFetchLimitOptionChange = (_, option: IDropdownOption) => {
|
||||||
this.setState({ fetchLimit: option.key as number })
|
this.setState({ fetchLimit: option.key as number })
|
||||||
}
|
}
|
||||||
|
|
||||||
handleInputChange = (event) => {
|
handleInputChange = event => {
|
||||||
const name: string = event.target.name
|
const name: string = event.target.name
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
this.setState({[name]: event.target.value})
|
this.setState({ [name]: event.target.value })
|
||||||
}
|
}
|
||||||
|
|
||||||
checkNotEmpty = (v: string) => {
|
checkNotEmpty = (v: string) => {
|
||||||
return (!this.state.existing && v.length == 0) ? intl.get("emptyField") : ""
|
return !this.state.existing && v.length == 0
|
||||||
|
? intl.get("emptyField")
|
||||||
|
: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
validateForm = () => {
|
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 () => {
|
save = async () => {
|
||||||
@ -64,10 +87,9 @@ class FeedbinConfigsTab extends React.Component<ServiceConfigsTabProps, FeedbinC
|
|||||||
configs = {
|
configs = {
|
||||||
...this.props.configs,
|
...this.props.configs,
|
||||||
endpoint: this.state.endpoint,
|
endpoint: this.state.endpoint,
|
||||||
fetchLimit: this.state.fetchLimit
|
fetchLimit: this.state.fetchLimit,
|
||||||
} as FeedbinConfigs
|
} as FeedbinConfigs
|
||||||
if (this.state.password)
|
if (this.state.password) configs.password = this.state.password
|
||||||
configs.password = this.state.password
|
|
||||||
} else {
|
} else {
|
||||||
configs = {
|
configs = {
|
||||||
type: SyncService.Feedbin,
|
type: SyncService.Feedbin,
|
||||||
@ -86,7 +108,10 @@ class FeedbinConfigsTab extends React.Component<ServiceConfigsTabProps, FeedbinC
|
|||||||
this.props.sync()
|
this.props.sync()
|
||||||
} else {
|
} else {
|
||||||
this.props.blockActions()
|
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() {
|
render() {
|
||||||
return <>
|
return (
|
||||||
{!this.state.existing && (
|
<>
|
||||||
<MessageBar messageBarType={MessageBarType.warning}>{intl.get("service.overwriteWarning")}</MessageBar>
|
{!this.state.existing && (
|
||||||
)}
|
<MessageBar messageBarType={MessageBarType.warning}>
|
||||||
<Stack horizontalAlign="center" style={{marginTop: 48}}>
|
{intl.get("service.overwriteWarning")}
|
||||||
<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>
|
</MessageBar>
|
||||||
<Label style={{margin: "8px 0 36px"}}>Feedbin</Label>
|
)}
|
||||||
<Stack className="login-form" horizontal>
|
<Stack horizontalAlign="center" style={{ marginTop: 48 }}>
|
||||||
<Stack.Item>
|
<svg
|
||||||
<Label>{intl.get("service.endpoint")}</Label>
|
style={{
|
||||||
</Stack.Item>
|
fill: "var(--black)",
|
||||||
<Stack.Item grow>
|
width: 32,
|
||||||
<TextField
|
userSelect: "none",
|
||||||
onGetErrorMessage={v => urlTest(v.trim()) ? "" : intl.get("sources.badUrl")}
|
}}
|
||||||
validateOnLoad={false}
|
viewBox="0 0 120 120">
|
||||||
name="endpoint"
|
<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" />
|
||||||
value={this.state.endpoint}
|
<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" />
|
||||||
onChange={this.handleInputChange} />
|
<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" />
|
||||||
</Stack.Item>
|
</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>
|
||||||
<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 { ServiceConfigsTabProps } from "../service"
|
||||||
import { FeverConfigs } from "../../../scripts/models/services/fever"
|
import { FeverConfigs } from "../../../scripts/models/services/fever"
|
||||||
import { SyncService } from "../../../schema-types"
|
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 DangerButton from "../../utils/danger-button"
|
||||||
import { urlTest } from "../../../scripts/utils"
|
import { urlTest } from "../../../scripts/utils"
|
||||||
import LiteExporter from "./lite-exporter"
|
import LiteExporter from "./lite-exporter"
|
||||||
@ -18,7 +30,10 @@ type FeverConfigsTabState = {
|
|||||||
importGroups: boolean
|
importGroups: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
class FeverConfigsTab extends React.Component<ServiceConfigsTabProps, FeverConfigsTabState> {
|
class FeverConfigsTab extends React.Component<
|
||||||
|
ServiceConfigsTabProps,
|
||||||
|
FeverConfigsTabState
|
||||||
|
> {
|
||||||
constructor(props: ServiceConfigsTabProps) {
|
constructor(props: ServiceConfigsTabProps) {
|
||||||
super(props)
|
super(props)
|
||||||
const configs = props.configs as FeverConfigs
|
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: 750, text: intl.get("service.fetchLimitNum", { count: 750 }) },
|
||||||
{ key: 1000, text: intl.get("service.fetchLimitNum", { count: 1000 }) },
|
{ key: 1000, text: intl.get("service.fetchLimitNum", { count: 1000 }) },
|
||||||
{ key: 1500, text: intl.get("service.fetchLimitNum", { count: 1500 }) },
|
{ 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) => {
|
onFetchLimitOptionChange = (_, option: IDropdownOption) => {
|
||||||
this.setState({ fetchLimit: option.key as number })
|
this.setState({ fetchLimit: option.key as number })
|
||||||
}
|
}
|
||||||
|
|
||||||
handleInputChange = (event) => {
|
handleInputChange = event => {
|
||||||
const name: string = event.target.name
|
const name: string = event.target.name
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
this.setState({[name]: event.target.value})
|
this.setState({ [name]: event.target.value })
|
||||||
}
|
}
|
||||||
|
|
||||||
checkNotEmpty = (v: string) => {
|
checkNotEmpty = (v: string) => {
|
||||||
return (!this.state.existing && v.length == 0) ? intl.get("emptyField") : ""
|
return !this.state.existing && v.length == 0
|
||||||
|
? intl.get("emptyField")
|
||||||
|
: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
validateForm = () => {
|
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 () => {
|
save = async () => {
|
||||||
@ -64,17 +88,19 @@ class FeverConfigsTab extends React.Component<ServiceConfigsTabProps, FeverConfi
|
|||||||
configs = {
|
configs = {
|
||||||
...this.props.configs,
|
...this.props.configs,
|
||||||
endpoint: this.state.endpoint,
|
endpoint: this.state.endpoint,
|
||||||
fetchLimit: this.state.fetchLimit
|
fetchLimit: this.state.fetchLimit,
|
||||||
} as FeverConfigs
|
} as FeverConfigs
|
||||||
if (this.state.password)
|
if (this.state.password)
|
||||||
configs.apiKey = md5(`${configs.username}:${this.state.password}`)
|
configs.apiKey = md5(
|
||||||
|
`${configs.username}:${this.state.password}`
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
configs = {
|
configs = {
|
||||||
type: SyncService.Fever,
|
type: SyncService.Fever,
|
||||||
endpoint: this.state.endpoint,
|
endpoint: this.state.endpoint,
|
||||||
username: this.state.username,
|
username: this.state.username,
|
||||||
fetchLimit: this.state.fetchLimit,
|
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
|
if (this.state.importGroups) configs.importGroups = true
|
||||||
}
|
}
|
||||||
@ -86,7 +112,10 @@ class FeverConfigsTab extends React.Component<ServiceConfigsTabProps, FeverConfi
|
|||||||
this.props.sync()
|
this.props.sync()
|
||||||
} else {
|
} else {
|
||||||
this.props.blockActions()
|
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() {
|
render() {
|
||||||
return <>
|
return (
|
||||||
{!this.state.existing && (
|
<>
|
||||||
<MessageBar messageBarType={MessageBarType.warning}>{intl.get("service.overwriteWarning")}</MessageBar>
|
{!this.state.existing && (
|
||||||
)}
|
<MessageBar messageBarType={MessageBarType.warning}>
|
||||||
<Stack horizontalAlign="center" style={{marginTop: 48}}>
|
{intl.get("service.overwriteWarning")}
|
||||||
<Icon iconName="Calories" style={{color: "var(--black)", fontSize: 32, userSelect: "none"}} />
|
</MessageBar>
|
||||||
<Label style={{margin: "8px 0 36px"}}>Fever API</Label>
|
)}
|
||||||
<Stack className="login-form" horizontal>
|
<Stack horizontalAlign="center" style={{ marginTop: 48 }}>
|
||||||
<Stack.Item>
|
<Icon
|
||||||
<Label>{intl.get("service.endpoint")}</Label>
|
iconName="Calories"
|
||||||
</Stack.Item>
|
style={{
|
||||||
<Stack.Item grow>
|
color: "var(--black)",
|
||||||
<TextField
|
fontSize: 32,
|
||||||
onGetErrorMessage={v => urlTest(v.trim()) ? "" : intl.get("sources.badUrl")}
|
userSelect: "none",
|
||||||
validateOnLoad={false}
|
}}
|
||||||
name="endpoint"
|
/>
|
||||||
value={this.state.endpoint}
|
<Label style={{ margin: "8px 0 36px" }}>Fever API</Label>
|
||||||
onChange={this.handleInputChange} />
|
<Stack className="login-form" horizontal>
|
||||||
</Stack.Item>
|
<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>
|
||||||
<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 { ServiceConfigsTabProps } from "../service"
|
||||||
import { GReaderConfigs } from "../../../scripts/models/services/greader"
|
import { GReaderConfigs } from "../../../scripts/models/services/greader"
|
||||||
import { SyncService } from "../../../schema-types"
|
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 DangerButton from "../../utils/danger-button"
|
||||||
import { urlTest } from "../../../scripts/utils"
|
import { urlTest } from "../../../scripts/utils"
|
||||||
import LiteExporter from "./lite-exporter"
|
import LiteExporter from "./lite-exporter"
|
||||||
@ -17,7 +29,10 @@ type GReaderConfigsTabState = {
|
|||||||
importGroups: boolean
|
importGroups: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
class GReaderConfigsTab extends React.Component<ServiceConfigsTabProps, GReaderConfigsTabState> {
|
class GReaderConfigsTab extends React.Component<
|
||||||
|
ServiceConfigsTabProps,
|
||||||
|
GReaderConfigsTabState
|
||||||
|
> {
|
||||||
constructor(props: ServiceConfigsTabProps) {
|
constructor(props: ServiceConfigsTabProps) {
|
||||||
super(props)
|
super(props)
|
||||||
const configs = props.configs as GReaderConfigs
|
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: 750, text: intl.get("service.fetchLimitNum", { count: 750 }) },
|
||||||
{ key: 1000, text: intl.get("service.fetchLimitNum", { count: 1000 }) },
|
{ key: 1000, text: intl.get("service.fetchLimitNum", { count: 1000 }) },
|
||||||
{ key: 1500, text: intl.get("service.fetchLimitNum", { count: 1500 }) },
|
{ 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) => {
|
onFetchLimitOptionChange = (_, option: IDropdownOption) => {
|
||||||
this.setState({ fetchLimit: option.key as number })
|
this.setState({ fetchLimit: option.key as number })
|
||||||
}
|
}
|
||||||
|
|
||||||
handleInputChange = (event) => {
|
handleInputChange = event => {
|
||||||
const name: string = event.target.name
|
const name: string = event.target.name
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
this.setState({[name]: event.target.value})
|
this.setState({ [name]: event.target.value })
|
||||||
}
|
}
|
||||||
|
|
||||||
checkNotEmpty = (v: string) => {
|
checkNotEmpty = (v: string) => {
|
||||||
return (!this.state.existing && v.length == 0) ? intl.get("emptyField") : ""
|
return !this.state.existing && v.length == 0
|
||||||
|
? intl.get("emptyField")
|
||||||
|
: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
validateForm = () => {
|
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 () => {
|
save = async () => {
|
||||||
@ -63,7 +87,7 @@ class GReaderConfigsTab extends React.Component<ServiceConfigsTabProps, GReaderC
|
|||||||
configs = {
|
configs = {
|
||||||
...this.props.configs,
|
...this.props.configs,
|
||||||
endpoint: this.state.endpoint,
|
endpoint: this.state.endpoint,
|
||||||
fetchLimit: this.state.fetchLimit
|
fetchLimit: this.state.fetchLimit,
|
||||||
} as GReaderConfigs
|
} as GReaderConfigs
|
||||||
if (this.state.password) configs.password = this.state.password
|
if (this.state.password) configs.password = this.state.password
|
||||||
} else {
|
} else {
|
||||||
@ -73,12 +97,12 @@ class GReaderConfigsTab extends React.Component<ServiceConfigsTabProps, GReaderC
|
|||||||
username: this.state.username,
|
username: this.state.username,
|
||||||
password: this.state.password,
|
password: this.state.password,
|
||||||
fetchLimit: this.state.fetchLimit,
|
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
|
if (this.state.importGroups) configs.importGroups = true
|
||||||
}
|
}
|
||||||
this.props.blockActions()
|
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)
|
const valid = await this.props.authenticate(configs)
|
||||||
if (valid) {
|
if (valid) {
|
||||||
this.props.save(configs)
|
this.props.save(configs)
|
||||||
@ -86,7 +110,10 @@ class GReaderConfigsTab extends React.Component<ServiceConfigsTabProps, GReaderC
|
|||||||
this.props.sync()
|
this.props.sync()
|
||||||
} else {
|
} else {
|
||||||
this.props.blockActions()
|
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() {
|
render() {
|
||||||
return <>
|
return (
|
||||||
{!this.state.existing && (
|
<>
|
||||||
<MessageBar messageBarType={MessageBarType.warning}>{intl.get("service.overwriteWarning")}</MessageBar>
|
{!this.state.existing && (
|
||||||
)}
|
<MessageBar messageBarType={MessageBarType.warning}>
|
||||||
<Stack horizontalAlign="center" style={{marginTop: 48}}>
|
{intl.get("service.overwriteWarning")}
|
||||||
<Icon iconName="Communications" style={{color: "var(--black)", transform: "rotate(220deg)", fontSize: 32, userSelect: "none"}} />
|
</MessageBar>
|
||||||
<Label style={{margin: "8px 0 36px"}}>Google Reader API</Label>
|
)}
|
||||||
<Stack className="login-form" horizontal>
|
<Stack horizontalAlign="center" style={{ marginTop: 48 }}>
|
||||||
<Stack.Item>
|
<Icon
|
||||||
<Label>{intl.get("service.endpoint")}</Label>
|
iconName="Communications"
|
||||||
</Stack.Item>
|
style={{
|
||||||
<Stack.Item grow>
|
color: "var(--black)",
|
||||||
<TextField
|
transform: "rotate(220deg)",
|
||||||
onGetErrorMessage={v => urlTest(v.trim()) ? "" : intl.get("sources.badUrl")}
|
fontSize: 32,
|
||||||
validateOnLoad={false}
|
userSelect: "none",
|
||||||
name="endpoint"
|
}}
|
||||||
value={this.state.endpoint}
|
/>
|
||||||
onChange={this.handleInputChange} />
|
<Label style={{ margin: "8px 0 36px" }}>
|
||||||
</Stack.Item>
|
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>
|
||||||
<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 { ServiceConfigsTabProps } from "../service"
|
||||||
import { GReaderConfigs } from "../../../scripts/models/services/greader"
|
import { GReaderConfigs } from "../../../scripts/models/services/greader"
|
||||||
import { SyncService } from "../../../schema-types"
|
import { SyncService } from "../../../schema-types"
|
||||||
import { Stack, Label, TextField, PrimaryButton, DefaultButton, Checkbox,
|
import {
|
||||||
MessageBar, MessageBarType, Dropdown, IDropdownOption, MessageBarButton, Link } from "@fluentui/react"
|
Stack,
|
||||||
|
Label,
|
||||||
|
TextField,
|
||||||
|
PrimaryButton,
|
||||||
|
DefaultButton,
|
||||||
|
Checkbox,
|
||||||
|
MessageBar,
|
||||||
|
MessageBarType,
|
||||||
|
Dropdown,
|
||||||
|
IDropdownOption,
|
||||||
|
MessageBarButton,
|
||||||
|
Link,
|
||||||
|
} from "@fluentui/react"
|
||||||
import DangerButton from "../../utils/danger-button"
|
import DangerButton from "../../utils/danger-button"
|
||||||
import LiteExporter from "./lite-exporter"
|
import LiteExporter from "./lite-exporter"
|
||||||
|
|
||||||
@ -22,12 +34,18 @@ type GReaderConfigsTabState = {
|
|||||||
const endpointOptions: IDropdownOption[] = [
|
const endpointOptions: IDropdownOption[] = [
|
||||||
"https://www.inoreader.com",
|
"https://www.inoreader.com",
|
||||||
"https://www.innoreader.com",
|
"https://www.innoreader.com",
|
||||||
"https://jp.inoreader.com"
|
"https://jp.inoreader.com",
|
||||||
].map(s => ({ key: s, text: s }))
|
].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) {
|
constructor(props: ServiceConfigsTabProps) {
|
||||||
super(props)
|
super(props)
|
||||||
const configs = props.configs as GReaderConfigs
|
const configs = props.configs as GReaderConfigs
|
||||||
@ -38,7 +56,10 @@ class InoreaderConfigsTab extends React.Component<ServiceConfigsTabProps, GReade
|
|||||||
password: "",
|
password: "",
|
||||||
apiId: configs.inoreaderId || "",
|
apiId: configs.inoreaderId || "",
|
||||||
apiKey: configs.inoreaderKey || "",
|
apiKey: configs.inoreaderKey || "",
|
||||||
removeAd: configs.removeInoreaderAd === undefined ? true : configs.removeInoreaderAd,
|
removeAd:
|
||||||
|
configs.removeInoreaderAd === undefined
|
||||||
|
? true
|
||||||
|
: configs.removeInoreaderAd,
|
||||||
fetchLimit: configs.fetchLimit || 250,
|
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: 500, text: intl.get("service.fetchLimitNum", { count: 500 }) },
|
||||||
{ key: 750, text: intl.get("service.fetchLimitNum", { count: 750 }) },
|
{ key: 750, text: intl.get("service.fetchLimitNum", { count: 750 }) },
|
||||||
{ key: 1000, text: intl.get("service.fetchLimitNum", { count: 1000 }) },
|
{ 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) => {
|
onFetchLimitOptionChange = (_, option: IDropdownOption) => {
|
||||||
this.setState({ fetchLimit: option.key as number })
|
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 })
|
this.setState({ endpoint: option.key as string })
|
||||||
}
|
}
|
||||||
|
|
||||||
handleInputChange = (event) => {
|
handleInputChange = event => {
|
||||||
const name: string = event.target.name
|
const name: string = event.target.name
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
this.setState({[name]: event.target.value})
|
this.setState({ [name]: event.target.value })
|
||||||
}
|
}
|
||||||
|
|
||||||
checkNotEmpty = (v: string) => {
|
checkNotEmpty = (v: string) => {
|
||||||
return (!this.state.existing && v.length == 0) ? intl.get("emptyField") : ""
|
return !this.state.existing && v.length == 0
|
||||||
|
? intl.get("emptyField")
|
||||||
|
: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
validateForm = () => {
|
validateForm = () => {
|
||||||
return (this.state.existing || (this.state.username && this.state.password))
|
return (
|
||||||
&& this.state.apiId && this.state.apiKey
|
(this.state.existing ||
|
||||||
|
(this.state.username && this.state.password)) &&
|
||||||
|
this.state.apiId &&
|
||||||
|
this.state.apiKey
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
save = async () => {
|
save = async () => {
|
||||||
@ -95,11 +125,11 @@ class InoreaderConfigsTab extends React.Component<ServiceConfigsTabProps, GReade
|
|||||||
removeInoreaderAd: this.state.removeAd,
|
removeInoreaderAd: this.state.removeAd,
|
||||||
fetchLimit: this.state.fetchLimit,
|
fetchLimit: this.state.fetchLimit,
|
||||||
importGroups: true,
|
importGroups: true,
|
||||||
useInt64: true
|
useInt64: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.props.blockActions()
|
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)
|
const valid = await this.props.authenticate(configs)
|
||||||
if (valid) {
|
if (valid) {
|
||||||
this.props.save(configs)
|
this.props.save(configs)
|
||||||
@ -107,11 +137,17 @@ class InoreaderConfigsTab extends React.Component<ServiceConfigsTabProps, GReade
|
|||||||
this.props.sync()
|
this.props.sync()
|
||||||
} else {
|
} else {
|
||||||
this.props.blockActions()
|
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 () => {
|
remove = async () => {
|
||||||
this.props.exit()
|
this.props.exit()
|
||||||
@ -119,118 +155,165 @@ class InoreaderConfigsTab extends React.Component<ServiceConfigsTabProps, GReade
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <>
|
return (
|
||||||
<MessageBar messageBarType={MessageBarType.severeWarning}
|
<>
|
||||||
isMultiline={false}
|
<MessageBar
|
||||||
actions={<MessageBarButton text={intl.get("create")} onClick={this.createKey} />}>
|
messageBarType={MessageBarType.severeWarning}
|
||||||
{intl.get("service.rateLimitWarning")}
|
isMultiline={false}
|
||||||
<Link onClick={openSupport} style={{marginLeft: 6}}>{intl.get("rules.help")}</Link>
|
actions={
|
||||||
</MessageBar>
|
<MessageBarButton
|
||||||
{!this.state.existing && (
|
text={intl.get("create")}
|
||||||
<MessageBar messageBarType={MessageBarType.warning}>{intl.get("service.overwriteWarning")}</MessageBar>
|
onClick={this.createKey}
|
||||||
)}
|
/>
|
||||||
<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>
|
{intl.get("service.rateLimitWarning")}
|
||||||
<Label style={{margin: "8px 0 36px"}}>Inoreader</Label>
|
<Link onClick={openSupport} style={{ marginLeft: 6 }}>
|
||||||
<Stack className="login-form" horizontal>
|
{intl.get("rules.help")}
|
||||||
<Stack.Item>
|
</Link>
|
||||||
<Label>{intl.get("service.endpoint")}</Label>
|
</MessageBar>
|
||||||
</Stack.Item>
|
{!this.state.existing && (
|
||||||
<Stack.Item grow>
|
<MessageBar messageBarType={MessageBarType.warning}>
|
||||||
<Dropdown
|
{intl.get("service.overwriteWarning")}
|
||||||
options={endpointOptions}
|
</MessageBar>
|
||||||
selectedKey={this.state.endpoint}
|
)}
|
||||||
onChange={this.onEndpointChange} />
|
<Stack horizontalAlign="center" style={{ marginTop: 48 }}>
|
||||||
</Stack.Item>
|
<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>
|
||||||
<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 * as React from "react"
|
||||||
import intl from "react-intl-universal"
|
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 { ServiceConfigs, SyncService } from "../../../schema-types"
|
||||||
import { renderShareQR } from "../../context-menu"
|
import { renderShareQR } from "../../context-menu"
|
||||||
import { platformCtrl } from "../../../scripts/utils"
|
import { platformCtrl } from "../../../scripts/utils"
|
||||||
@ -12,9 +18,10 @@ type LiteExporterProps = {
|
|||||||
serviceConfigs: ServiceConfigs
|
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/?"
|
let url = "https://hyliu.me/fr2l/?"
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
switch (props.serviceConfigs.type) {
|
switch (props.serviceConfigs.type) {
|
||||||
@ -58,16 +65,21 @@ const LiteExporter: React.FunctionComponent<LiteExporterProps> = (props) => {
|
|||||||
key: "openInBrowser",
|
key: "openInBrowser",
|
||||||
text: intl.get("rules.help"),
|
text: intl.get("rules.help"),
|
||||||
iconProps: { iconName: "NavigateExternalInline" },
|
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}}>
|
return (
|
||||||
<DefaultButton
|
<Stack style={{ marginTop: 32 }}>
|
||||||
text={intl.get("service.exportToLite")}
|
<DefaultButton
|
||||||
onRenderMenuIcon={() => <></>}
|
text={intl.get("service.exportToLite")}
|
||||||
menuProps={menuProps} />
|
onRenderMenuIcon={() => <></>}
|
||||||
</Stack>
|
menuProps={menuProps}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LiteExporter
|
export default LiteExporter
|
||||||
|
@ -1,9 +1,27 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import intl from "react-intl-universal"
|
import intl from "react-intl-universal"
|
||||||
import { Label, DefaultButton, TextField, Stack, PrimaryButton, DetailsList,
|
import {
|
||||||
IColumn, SelectionMode, Selection, IChoiceGroupOption, ChoiceGroup, IDropdownOption,
|
Label,
|
||||||
Dropdown, MessageBar, MessageBarType } from "@fluentui/react"
|
DefaultButton,
|
||||||
import { SourceState, RSSSource, SourceOpenTarget } from "../../scripts/models/source"
|
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 { urlTest } from "../../scripts/utils"
|
||||||
import DangerButton from "../utils/danger-button"
|
import DangerButton from "../utils/danger-button"
|
||||||
|
|
||||||
@ -15,7 +33,10 @@ type SourcesTabProps = {
|
|||||||
addSource: (url: string) => void
|
addSource: (url: string) => void
|
||||||
updateSourceName: (source: RSSSource, name: string) => void
|
updateSourceName: (source: RSSSource, name: string) => void
|
||||||
updateSourceIcon: (source: RSSSource, iconUrl: string) => Promise<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
|
updateFetchFrequency: (source: RSSSource, frequency: number) => void
|
||||||
deleteSource: (source: RSSSource) => void
|
deleteSource: (source: RSSSource) => void
|
||||||
deleteSources: (sources: RSSSource[]) => void
|
deleteSources: (sources: RSSSource[]) => void
|
||||||
@ -26,7 +47,7 @@ type SourcesTabProps = {
|
|||||||
type SourcesTabState = {
|
type SourcesTabState = {
|
||||||
[formName: string]: string
|
[formName: string]: string
|
||||||
} & {
|
} & {
|
||||||
selectedSource: RSSSource,
|
selectedSource: RSSSource
|
||||||
selectedSources: RSSSource[]
|
selectedSources: RSSSource[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,21 +66,23 @@ class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
|
|||||||
newUrl: "",
|
newUrl: "",
|
||||||
newSourceName: "",
|
newSourceName: "",
|
||||||
selectedSource: null,
|
selectedSource: null,
|
||||||
selectedSources: null
|
selectedSources: null,
|
||||||
}
|
}
|
||||||
this.selection = new Selection({
|
this.selection = new Selection({
|
||||||
getKey: s => (s as RSSSource).sid,
|
getKey: s => (s as RSSSource).sid,
|
||||||
onSelectionChanged: () => {
|
onSelectionChanged: () => {
|
||||||
let count = this.selection.getSelectedCount()
|
let count = this.selection.getSelectedCount()
|
||||||
let sources = count ? this.selection.getSelection() as RSSSource[] : null
|
let sources = count
|
||||||
|
? (this.selection.getSelection() as RSSSource[])
|
||||||
|
: null
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedSource: count === 1 ? sources[0] : null,
|
selectedSource: count === 1 ? sources[0] : null,
|
||||||
selectedSources: count > 1 ? sources : null,
|
selectedSources: count > 1 ? sources : null,
|
||||||
newSourceName: count === 1 ? sources[0].name : "",
|
newSourceName: count === 1 ? sources[0].name : "",
|
||||||
newSourceIcon: count === 1 ? (sources[0].iconurl || "") : "",
|
newSourceIcon: count === 1 ? sources[0].iconurl || "" : "",
|
||||||
sourceEditOption: EditDropdownKeys.Name,
|
sourceEditOption: EditDropdownKeys.Name,
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,25 +104,24 @@ class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
|
|||||||
iconName: "ImagePixel",
|
iconName: "ImagePixel",
|
||||||
minWidth: 16,
|
minWidth: 16,
|
||||||
maxWidth: 16,
|
maxWidth: 16,
|
||||||
onRender: (s: RSSSource) => s.iconurl && (
|
onRender: (s: RSSSource) =>
|
||||||
<img src={s.iconurl} className="favicon" />
|
s.iconurl && <img src={s.iconurl} className="favicon" />,
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "name",
|
key: "name",
|
||||||
name: intl.get("name"),
|
name: intl.get("name"),
|
||||||
fieldName: "name",
|
fieldName: "name",
|
||||||
minWidth: 200,
|
minWidth: 200,
|
||||||
data: 'string',
|
data: "string",
|
||||||
isRowHeader: true
|
isRowHeader: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "url",
|
key: "url",
|
||||||
name: "URL",
|
name: "URL",
|
||||||
fieldName: "url",
|
fieldName: "url",
|
||||||
minWidth: 280,
|
minWidth: 280,
|
||||||
data: 'string'
|
data: "string",
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
sourceEditOptions = (): IDropdownOption[] => [
|
sourceEditOptions = (): IDropdownOption[] => [
|
||||||
@ -109,7 +131,7 @@ class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
|
|||||||
]
|
]
|
||||||
|
|
||||||
onSourceEditOptionChange = (_, option: IDropdownOption) => {
|
onSourceEditOptionChange = (_, option: IDropdownOption) => {
|
||||||
this.setState({sourceEditOption: option.key as string})
|
this.setState({ sourceEditOption: option.key as string })
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchFrequencyOptions = (): IDropdownOption[] => [
|
fetchFrequencyOptions = (): IDropdownOption[] => [
|
||||||
@ -121,37 +143,61 @@ class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
|
|||||||
{ key: "180", text: intl.get("time.hour", { h: 3 }) },
|
{ key: "180", text: intl.get("time.hour", { h: 3 }) },
|
||||||
{ key: "360", text: intl.get("time.hour", { h: 6 }) },
|
{ key: "360", text: intl.get("time.hour", { h: 6 }) },
|
||||||
{ key: "720", text: intl.get("time.hour", { h: 12 }) },
|
{ 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) => {
|
onFetchFrequencyChange = (_, option: IDropdownOption) => {
|
||||||
let frequency = parseInt(option.key as string)
|
let frequency = parseInt(option.key as string)
|
||||||
this.props.updateFetchFrequency(this.state.selectedSource, frequency)
|
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[] => [
|
sourceOpenTargetChoices = (): IChoiceGroupOption[] => [
|
||||||
{ key: String(SourceOpenTarget.Local), text: intl.get("sources.rssText") },
|
{
|
||||||
{ key: String(SourceOpenTarget.FullContent), text: intl.get("article.loadFull") },
|
key: String(SourceOpenTarget.Local),
|
||||||
{ key: String(SourceOpenTarget.Webpage), text: intl.get("sources.loadWebpage") },
|
text: intl.get("sources.rssText"),
|
||||||
{ key: String(SourceOpenTarget.External), text: intl.get("openExternal") }
|
},
|
||||||
|
{
|
||||||
|
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 = () => {
|
updateSourceName = () => {
|
||||||
let newName = this.state.newSourceName.trim()
|
let newName = this.state.newSourceName.trim()
|
||||||
this.props.updateSourceName(this.state.selectedSource, newName)
|
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 = () => {
|
updateSourceIcon = () => {
|
||||||
let newIcon = this.state.newSourceIcon.trim()
|
let newIcon = this.state.newSourceIcon.trim()
|
||||||
this.props.updateSourceIcon(this.state.selectedSource, newIcon)
|
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
|
const name: string = event.target.name
|
||||||
this.setState({[name]: event.target.value})
|
this.setState({ [name]: event.target.value })
|
||||||
}
|
}
|
||||||
|
|
||||||
addSource = (event: React.FormEvent) => {
|
addSource = (event: React.FormEvent) => {
|
||||||
@ -163,164 +209,258 @@ class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
|
|||||||
onOpenTargetChange = (_, option: IChoiceGroupOption) => {
|
onOpenTargetChange = (_, option: IChoiceGroupOption) => {
|
||||||
let newTarget = parseInt(option.key) as SourceOpenTarget
|
let newTarget = parseInt(option.key) as SourceOpenTarget
|
||||||
this.props.updateSourceOpenTarget(this.state.selectedSource, newTarget)
|
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 = () => (
|
render = () => (
|
||||||
<div className="tab-body">
|
<div className="tab-body">
|
||||||
{this.props.serviceOn && (
|
{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>
|
<Label>{intl.get("sources.opmlFile")}</Label>
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<Stack.Item>
|
<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>
|
||||||
<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.Item>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<form onSubmit={this.addSource}>
|
<form onSubmit={this.addSource}>
|
||||||
<Label htmlFor="newUrl">{intl.get("sources.add")}</Label>
|
<Label htmlFor="newUrl">{intl.get("sources.add")}</Label>
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<Stack.Item grow>
|
<Stack.Item grow>
|
||||||
<TextField
|
<TextField
|
||||||
onGetErrorMessage={v => urlTest(v.trim()) ? "" : intl.get("sources.badUrl")}
|
onGetErrorMessage={v =>
|
||||||
validateOnLoad={false}
|
urlTest(v.trim())
|
||||||
|
? ""
|
||||||
|
: intl.get("sources.badUrl")
|
||||||
|
}
|
||||||
|
validateOnLoad={false}
|
||||||
placeholder={intl.get("sources.inputUrl")}
|
placeholder={intl.get("sources.inputUrl")}
|
||||||
value={this.state.newUrl}
|
value={this.state.newUrl}
|
||||||
id="newUrl"
|
id="newUrl"
|
||||||
name="newUrl"
|
name="newUrl"
|
||||||
onChange={this.handleInputChange} />
|
onChange={this.handleInputChange}
|
||||||
|
/>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<Stack.Item>
|
<Stack.Item>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
disabled={!urlTest(this.state.newUrl.trim())}
|
disabled={!urlTest(this.state.newUrl.trim())}
|
||||||
type="submit"
|
type="submit"
|
||||||
text={intl.get("add")} />
|
text={intl.get("add")}
|
||||||
|
/>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DetailsList
|
<DetailsList
|
||||||
compact={Object.keys(this.props.sources).length >= 10}
|
compact={Object.keys(this.props.sources).length >= 10}
|
||||||
items={Object.values(this.props.sources)}
|
items={Object.values(this.props.sources)}
|
||||||
columns={this.columns()}
|
columns={this.columns()}
|
||||||
getKey={s => s.sid}
|
getKey={s => s.sid}
|
||||||
setKey="selected"
|
setKey="selected"
|
||||||
selection={this.selection}
|
selection={this.selection}
|
||||||
selectionMode={SelectionMode.multiple} />
|
selectionMode={SelectionMode.multiple}
|
||||||
|
/>
|
||||||
|
|
||||||
{this.state.selectedSource && <>
|
{this.state.selectedSource && (
|
||||||
{this.state.selectedSource.serviceRef && (
|
<>
|
||||||
<MessageBar messageBarType={MessageBarType.info}>{intl.get("sources.serviceManaged")}</MessageBar>
|
{this.state.selectedSource.serviceRef && (
|
||||||
)}
|
<MessageBar messageBarType={MessageBarType.info}>
|
||||||
<Label>{intl.get("sources.selected")}</Label>
|
{intl.get("sources.serviceManaged")}
|
||||||
<Stack horizontal>
|
</MessageBar>
|
||||||
<Stack.Item>
|
)}
|
||||||
<Dropdown
|
<Label>{intl.get("sources.selected")}</Label>
|
||||||
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 && (
|
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<Stack.Item>
|
<Stack.Item>
|
||||||
<DangerButton
|
<Dropdown
|
||||||
onClick={() => this.props.deleteSource(this.state.selectedSource)}
|
options={this.sourceEditOptions()}
|
||||||
key={this.state.selectedSource.sid}
|
selectedKey={this.state.sourceEditOption}
|
||||||
text={intl.get("sources.delete")} />
|
onChange={this.onSourceEditOptionChange}
|
||||||
</Stack.Item>
|
style={{ width: 120 }}
|
||||||
<Stack.Item>
|
/>
|
||||||
<span className="settings-hint">{intl.get("sources.deleteWarning")}</span>
|
|
||||||
</Stack.Item>
|
</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>
|
</Stack>
|
||||||
)}
|
{!this.state.selectedSource.serviceRef && (
|
||||||
</>}
|
<>
|
||||||
{this.state.selectedSources && (this.state.selectedSources.filter(s => s.serviceRef).length === 0
|
<Label>{intl.get("sources.fetchFrequency")}</Label>
|
||||||
? <>
|
<Stack>
|
||||||
<Label>{intl.get("sources.selectedMulti")}</Label>
|
<Stack.Item>
|
||||||
<Stack horizontal>
|
<Dropdown
|
||||||
<Stack.Item>
|
options={this.fetchFrequencyOptions()}
|
||||||
<DangerButton
|
selectedKey={
|
||||||
onClick={() => this.props.deleteSources(this.state.selectedSources)}
|
this.state.selectedSource
|
||||||
text={intl.get("sources.delete")} />
|
.fetchFrequency
|
||||||
</Stack.Item>
|
? String(
|
||||||
<Stack.Item>
|
this.state.selectedSource
|
||||||
<span className="settings-hint">{intl.get("sources.deleteWarning")}</span>
|
.fetchFrequency
|
||||||
</Stack.Item>
|
)
|
||||||
</Stack>
|
: "0"
|
||||||
</>
|
}
|
||||||
: (
|
onChange={this.onFetchFrequencyChange}
|
||||||
<MessageBar messageBarType={MessageBarType.info}>{intl.get("sources.serviceManaged")}</MessageBar>
|
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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SourcesTab
|
export default SourcesTab
|
||||||
|
182
src/components/utils/ResizeObserver.d.ts
vendored
182
src/components/utils/ResizeObserver.d.ts
vendored
@ -70,8 +70,8 @@ declare class ResizeObserver {
|
|||||||
*
|
*
|
||||||
* resizeObserver.observe(divElem);
|
* resizeObserver.observe(divElem);
|
||||||
*/
|
*/
|
||||||
constructor(callback: ResizeObserverCallback);
|
constructor(callback: ResizeObserverCallback)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The **disconnect()** method of the
|
* The **disconnect()** method of the
|
||||||
* [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)
|
* [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)
|
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement)
|
||||||
* targets.
|
* targets.
|
||||||
*/
|
*/
|
||||||
disconnect: () => void;
|
disconnect: () => void
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `observe()` method of the
|
* The `observe()` method of the
|
||||||
* [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)
|
* [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.
|
* An options object allowing you to set options for the observation.
|
||||||
* Currently this only has one possible option that can be set.
|
* 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
|
* The **unobserve()** method of the
|
||||||
* [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)
|
* [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
|
* [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or
|
||||||
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement).
|
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement).
|
||||||
*/
|
*/
|
||||||
unobserve: (target: Element) => void;
|
unobserve: (target: Element) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ResizeObserverObserveOptions {
|
interface ResizeObserverObserveOptions {
|
||||||
/**
|
/**
|
||||||
* Sets which box model the observer will observe changes to. Possible values
|
* Sets which box model the observer will observe changes to. Possible values
|
||||||
* are `content-box` (the default), and `border-box`.
|
* are `content-box` (the default), and `border-box`.
|
||||||
*
|
*
|
||||||
* @default "content-box"
|
* @default "content-box"
|
||||||
*/
|
*/
|
||||||
box?: "content-box" | "border-box";
|
box?: "content-box" | "border-box"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The function called whenever an observed resize occurs. The function is
|
* The function called whenever an observed resize occurs. The function is
|
||||||
* called with two parameters:
|
* called with two parameters:
|
||||||
*
|
*
|
||||||
* @param entries
|
* @param entries
|
||||||
* An array of
|
* An array of
|
||||||
* [ResizeObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry)
|
* [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
|
* objects that can be used to access the new dimensions of the element after
|
||||||
* each change.
|
* each change.
|
||||||
*
|
*
|
||||||
* @param observer
|
* @param observer
|
||||||
* A reference to the `ResizeObserver` itself, so it will definitely be
|
* A reference to the `ResizeObserver` itself, so it will definitely be
|
||||||
* accessible from inside the callback, should you need it. This could be used
|
* accessible from inside the callback, should you need it. This could be used
|
||||||
* for example to automatically unobserve the observer when a certain condition
|
* for example to automatically unobserve the observer when a certain condition
|
||||||
* is reached, but you can omit it if you don't need it.
|
* is reached, but you can omit it if you don't need it.
|
||||||
*
|
*
|
||||||
* The callback will generally follow a pattern along the lines of:
|
* The callback will generally follow a pattern along the lines of:
|
||||||
* @example
|
* @example
|
||||||
* function(entries, observer) {
|
* function(entries, observer) {
|
||||||
* for (let entry of entries) {
|
* for (let entry of entries) {
|
||||||
* // Do something to each entry
|
* // Do something to each entry
|
||||||
* // and possibly something to the observer itself
|
* // and possibly something to the observer itself
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const resizeObserver = new ResizeObserver(entries => {
|
* const resizeObserver = new ResizeObserver(entries => {
|
||||||
* for (let entry of entries) {
|
* for (let entry of entries) {
|
||||||
* if(entry.contentBoxSize) {
|
* if(entry.contentBoxSize) {
|
||||||
* h1Elem.style.fontSize = Math.max(1.5, entry.contentBoxSize.inlineSize/200) + 'rem';
|
* h1Elem.style.fontSize = Math.max(1.5, entry.contentBoxSize.inlineSize/200) + 'rem';
|
||||||
* pElem.style.fontSize = Math.max(1, entry.contentBoxSize.inlineSize/600) + 'rem';
|
* pElem.style.fontSize = Math.max(1, entry.contentBoxSize.inlineSize/600) + 'rem';
|
||||||
* } else {
|
* } else {
|
||||||
* h1Elem.style.fontSize = Math.max(1.5, entry.contentRect.width/200) + 'rem';
|
* h1Elem.style.fontSize = Math.max(1.5, entry.contentRect.width/200) + 'rem';
|
||||||
* pElem.style.fontSize = Math.max(1, entry.contentRect.width/600) + 'rem';
|
* pElem.style.fontSize = Math.max(1, entry.contentRect.width/600) + 'rem';
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
* });
|
* });
|
||||||
*
|
*
|
||||||
* resizeObserver.observe(divElem);
|
* resizeObserver.observe(divElem);
|
||||||
*/
|
*/
|
||||||
type ResizeObserverCallback = (
|
type ResizeObserverCallback = (
|
||||||
entries: ResizeObserverEntry[],
|
entries: ResizeObserverEntry[],
|
||||||
observer: ResizeObserver,
|
observer: ResizeObserver
|
||||||
) => void;
|
) => void
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The **ResizeObserverEntry** interface represents the object passed to the
|
* The **ResizeObserverEntry** interface represents the object passed to the
|
||||||
* [ResizeObserver()](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/ResizeObserver)
|
* [ResizeObserver()](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/ResizeObserver)
|
||||||
* constructor's callback function, which allows you to access the new
|
* constructor's callback function, which allows you to access the new
|
||||||
* dimensions of the
|
* dimensions of the
|
||||||
* [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or
|
* [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or
|
||||||
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement)
|
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement)
|
||||||
* being observed.
|
* being observed.
|
||||||
*/
|
*/
|
||||||
interface ResizeObserverEntry {
|
interface ResizeObserverEntry {
|
||||||
/**
|
/**
|
||||||
* An object containing the new border box size of the observed element when
|
* An object containing the new border box size of the observed element when
|
||||||
* the callback is run.
|
* the callback is run.
|
||||||
*/
|
*/
|
||||||
readonly borderBoxSize: ResizeObserverEntryBoxSize;
|
readonly borderBoxSize: ResizeObserverEntryBoxSize
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An object containing the new content box size of the observed element when
|
* An object containing the new content box size of the observed element when
|
||||||
* the callback is run.
|
* the callback is run.
|
||||||
*/
|
*/
|
||||||
readonly contentBoxSize: ResizeObserverEntryBoxSize;
|
readonly contentBoxSize: ResizeObserverEntryBoxSize
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [DOMRectReadOnly](https://developer.mozilla.org/en-US/docs/Web/API/DOMRectReadOnly)
|
* 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
|
* object containing the new size of the observed element when the callback is
|
||||||
@ -200,24 +200,24 @@ declare class ResizeObserver {
|
|||||||
* in future versions.
|
* in future versions.
|
||||||
*/
|
*/
|
||||||
// node_modules/typescript/lib/lib.dom.d.ts
|
// node_modules/typescript/lib/lib.dom.d.ts
|
||||||
readonly contentRect: DOMRectReadOnly;
|
readonly contentRect: DOMRectReadOnly
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A reference to the
|
* A reference to the
|
||||||
* [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or
|
* [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or
|
||||||
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement)
|
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement)
|
||||||
* being observed.
|
* being observed.
|
||||||
*/
|
*/
|
||||||
readonly target: Element;
|
readonly target: Element
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The **borderBoxSize** read-only property of the
|
* The **borderBoxSize** read-only property of the
|
||||||
* [ResizeObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry)
|
* [ResizeObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry)
|
||||||
* interface returns an object containing the new border box size of the
|
* interface returns an object containing the new border box size of the
|
||||||
* observed element when the callback is run.
|
* observed element when the callback is run.
|
||||||
*/
|
*/
|
||||||
interface ResizeObserverEntryBoxSize {
|
interface ResizeObserverEntryBoxSize {
|
||||||
/**
|
/**
|
||||||
* The length of the observed element's border box in the block dimension. For
|
* The length of the observed element's border box in the block dimension. For
|
||||||
* boxes with a horizontal
|
* 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 vertical dimension, or height; if the writing-mode is vertical,
|
||||||
* this is the horizontal dimension, or width.
|
* this is the horizontal dimension, or width.
|
||||||
*/
|
*/
|
||||||
blockSize: number;
|
blockSize: number
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The length of the observed element's border box in the inline dimension.
|
* The length of the observed element's border box in the inline dimension.
|
||||||
* For boxes with a horizontal
|
* For boxes with a horizontal
|
||||||
@ -234,9 +234,9 @@ declare class ResizeObserver {
|
|||||||
* this is the horizontal dimension, or width; if the writing-mode is
|
* this is the horizontal dimension, or width; if the writing-mode is
|
||||||
* vertical, this is the vertical dimension, or height.
|
* vertical, this is the vertical dimension, or height.
|
||||||
*/
|
*/
|
||||||
inlineSize: number;
|
inlineSize: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
ResizeObserver: typeof ResizeObserver;
|
ResizeObserver: typeof ResizeObserver
|
||||||
}
|
}
|
||||||
|
@ -43,19 +43,22 @@ class ArticleSearch extends React.Component<SearchProps, SearchState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return this.props.searchOn && (
|
return (
|
||||||
<SearchBox
|
this.props.searchOn && (
|
||||||
componentRef={this.inputRef}
|
<SearchBox
|
||||||
className="article-search"
|
componentRef={this.inputRef}
|
||||||
placeholder={intl.get("search")}
|
className="article-search"
|
||||||
value={this.state.query}
|
placeholder={intl.get("search")}
|
||||||
onChange={this.onSearchChange} />
|
value={this.state.query}
|
||||||
|
onChange={this.onSearchChange}
|
||||||
|
/>
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSearchProps = (state: RootState) => ({
|
const getSearchProps = (state: RootState) => ({
|
||||||
searchOn: state.page.searchOn,
|
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 * as React from "react"
|
||||||
import intl from "react-intl-universal"
|
import intl from "react-intl-universal"
|
||||||
import { PrimaryButton } from "@fluentui/react";
|
import { PrimaryButton } from "@fluentui/react"
|
||||||
|
|
||||||
class DangerButton extends PrimaryButton {
|
class DangerButton extends PrimaryButton {
|
||||||
timerID: NodeJS.Timeout
|
timerID: NodeJS.Timeout
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
confirming: false
|
confirming: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
clear = () => {
|
clear = () => {
|
||||||
@ -34,15 +34,20 @@ class DangerButton extends PrimaryButton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render = () => (
|
render = () => (
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
{...this.props}
|
{...this.props}
|
||||||
className={this.props.className + " danger"}
|
className={this.props.className + " danger"}
|
||||||
onClick={this.onClick}
|
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}
|
{this.props.children}
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DangerButton
|
export default DangerButton
|
||||||
|
@ -10,18 +10,15 @@ class Time extends React.Component<TimeProps> {
|
|||||||
state = { now: new Date() }
|
state = { now: new Date() }
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.timerID = setInterval(
|
this.timerID = setInterval(() => this.tick(), 60000)
|
||||||
() => this.tick(),
|
}
|
||||||
60000
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
clearInterval(this.timerID)
|
clearInterval(this.timerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
tick() {
|
tick() {
|
||||||
this.setState({ now: new Date() });
|
this.setState({ now: new Date() })
|
||||||
}
|
}
|
||||||
|
|
||||||
displayTime(past: Date, now: Date): string {
|
displayTime(past: Date, now: Date): string {
|
||||||
@ -35,9 +32,11 @@ class Time extends React.Component<TimeProps> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
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 { connect } from "react-redux"
|
||||||
import { createSelector } from "reselect"
|
import { createSelector } from "reselect"
|
||||||
import { RootState } from "../scripts/reducer"
|
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 { AppDispatch } from "../scripts/utils"
|
||||||
import { dismissItem, showOffsetItem } from "../scripts/models/page"
|
import { dismissItem, showOffsetItem } from "../scripts/models/page"
|
||||||
import Article from "../components/article"
|
import Article from "../components/article"
|
||||||
import { openTextMenu, closeContextMenu, openImageMenu } from "../scripts/models/app"
|
import {
|
||||||
|
openTextMenu,
|
||||||
|
closeContextMenu,
|
||||||
|
openImageMenu,
|
||||||
|
} from "../scripts/models/app"
|
||||||
|
|
||||||
type ArticleContainerProps = {
|
type ArticleContainerProps = {
|
||||||
itemId: number
|
itemId: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const getItem = (state: RootState, props: ArticleContainerProps) => state.items[props.itemId]
|
const getItem = (state: RootState, props: ArticleContainerProps) =>
|
||||||
const getSource = (state: RootState, props: ArticleContainerProps) => state.sources[state.items[props.itemId].source]
|
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 getLocale = (state: RootState) => state.app.locale
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
const makeMapStateToProps = () => {
|
||||||
@ -21,28 +34,35 @@ const makeMapStateToProps = () => {
|
|||||||
(item, source, locale) => ({
|
(item, source, locale) => ({
|
||||||
item: item,
|
item: item,
|
||||||
source: source,
|
source: source,
|
||||||
locale: locale
|
locale: locale,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch: AppDispatch) => {
|
const mapDispatchToProps = (dispatch: AppDispatch) => {
|
||||||
return {
|
return {
|
||||||
shortcuts: (item: RSSItem, e: KeyboardEvent) => dispatch(itemShortcuts(item, e)),
|
shortcuts: (item: RSSItem, e: KeyboardEvent) =>
|
||||||
|
dispatch(itemShortcuts(item, e)),
|
||||||
dismiss: () => dispatch(dismissItem()),
|
dismiss: () => dispatch(dismissItem()),
|
||||||
offsetItem: (offset: number) => dispatch(showOffsetItem(offset)),
|
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)),
|
toggleStarred: (item: RSSItem) => dispatch(toggleStarred(item)),
|
||||||
toggleHidden: (item: RSSItem) => {
|
toggleHidden: (item: RSSItem) => {
|
||||||
if (!item.hidden) dispatch(dismissItem())
|
if (!item.hidden) dispatch(dismissItem())
|
||||||
if (!item.hasRead && !item.hidden) dispatch(markRead(item))
|
if (!item.hasRead && !item.hidden) dispatch(markRead(item))
|
||||||
dispatch(toggleHidden(item))
|
dispatch(toggleHidden(item))
|
||||||
},
|
},
|
||||||
textMenu: (position: [number, number], text: string, url: string) => dispatch(openTextMenu(position, text, url)),
|
textMenu: (position: [number, number], text: string, url: string) =>
|
||||||
imageMenu: (position: [number, number]) => dispatch(openImageMenu(position)),
|
dispatch(openTextMenu(position, text, url)),
|
||||||
dismissContextMenu: () => dispatch(closeContextMenu())
|
imageMenu: (position: [number, number]) =>
|
||||||
|
dispatch(openImageMenu(position)),
|
||||||
|
dismissContextMenu: () => dispatch(closeContextMenu()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ArticleContainer = connect(makeMapStateToProps, mapDispatchToProps)(Article)
|
const ArticleContainer = connect(
|
||||||
export default ArticleContainer
|
makeMapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(Article)
|
||||||
|
export default ArticleContainer
|
||||||
|
@ -1,10 +1,28 @@
|
|||||||
import { connect } from "react-redux"
|
import { connect } from "react-redux"
|
||||||
import { createSelector } from "reselect"
|
import { createSelector } from "reselect"
|
||||||
import { RootState } from "../scripts/reducer"
|
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 { ContextMenu } from "../components/context-menu"
|
||||||
import { RSSItem, markRead, markUnread, toggleStarred, toggleHidden, markAllRead, fetchItems } from "../scripts/models/item"
|
import {
|
||||||
import { showItem, switchView, switchFilter, toggleFilter, setViewConfigs } from "../scripts/models/page"
|
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 { ViewType, ViewConfigs } from "../schema-types"
|
||||||
import { FilterType } from "../scripts/models/feed"
|
import { FilterType } from "../scripts/models/feed"
|
||||||
|
|
||||||
@ -17,51 +35,59 @@ const mapStateToProps = createSelector(
|
|||||||
[getContext, getViewType, getFilter, getViewConfigs],
|
[getContext, getViewType, getFilter, getViewConfigs],
|
||||||
(context, viewType, filter, viewConfigs) => {
|
(context, viewType, filter, viewConfigs) => {
|
||||||
switch (context.type) {
|
switch (context.type) {
|
||||||
case ContextMenuType.Item: return {
|
case ContextMenuType.Item:
|
||||||
type: context.type,
|
return {
|
||||||
event: context.event,
|
type: context.type,
|
||||||
viewConfigs: viewConfigs,
|
event: context.event,
|
||||||
item: context.target[0],
|
viewConfigs: viewConfigs,
|
||||||
feedId: context.target[1]
|
item: context.target[0],
|
||||||
}
|
feedId: context.target[1],
|
||||||
case ContextMenuType.Text: return {
|
}
|
||||||
type: context.type,
|
case ContextMenuType.Text:
|
||||||
position: context.position,
|
return {
|
||||||
text: context.target[0],
|
type: context.type,
|
||||||
url: context.target[1]
|
position: context.position,
|
||||||
}
|
text: context.target[0],
|
||||||
case ContextMenuType.View: return {
|
url: context.target[1],
|
||||||
type: context.type,
|
}
|
||||||
event: context.event,
|
case ContextMenuType.View:
|
||||||
viewType: viewType,
|
return {
|
||||||
filter: filter.type
|
type: context.type,
|
||||||
}
|
event: context.event,
|
||||||
case ContextMenuType.Group: return {
|
viewType: viewType,
|
||||||
type: context.type,
|
filter: filter.type,
|
||||||
event: context.event,
|
}
|
||||||
sids: context.target
|
case ContextMenuType.Group:
|
||||||
}
|
return {
|
||||||
case ContextMenuType.Image: return {
|
type: context.type,
|
||||||
type: context.type,
|
event: context.event,
|
||||||
position: context.position
|
sids: context.target,
|
||||||
}
|
}
|
||||||
case ContextMenuType.MarkRead: return {
|
case ContextMenuType.Image:
|
||||||
type: context.type,
|
return {
|
||||||
event: context.event
|
type: context.type,
|
||||||
}
|
position: context.position,
|
||||||
default: return { type: ContextMenuType.Hidden }
|
}
|
||||||
|
case ContextMenuType.MarkRead:
|
||||||
|
return {
|
||||||
|
type: context.type,
|
||||||
|
event: context.event,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return { type: ContextMenuType.Hidden }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = dispatch => {
|
||||||
return {
|
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)),
|
markRead: (item: RSSItem) => dispatch(markRead(item)),
|
||||||
markUnread: (item: RSSItem) => dispatch(markUnread(item)),
|
markUnread: (item: RSSItem) => dispatch(markUnread(item)),
|
||||||
toggleStarred: (item: RSSItem) => dispatch(toggleStarred(item)),
|
toggleStarred: (item: RSSItem) => dispatch(toggleStarred(item)),
|
||||||
toggleHidden: (item: RSSItem) => {
|
toggleHidden: (item: RSSItem) => {
|
||||||
if(!item.hasRead) {
|
if (!item.hasRead) {
|
||||||
dispatch(markRead(item))
|
dispatch(markRead(item))
|
||||||
item.hasRead = true // get around chaining error
|
item.hasRead = true // get around chaining error
|
||||||
}
|
}
|
||||||
@ -71,7 +97,8 @@ const mapDispatchToProps = dispatch => {
|
|||||||
window.settings.setDefaultView(viewType)
|
window.settings.setDefaultView(viewType)
|
||||||
dispatch(switchView(viewType))
|
dispatch(switchView(viewType))
|
||||||
},
|
},
|
||||||
setViewConfigs: (configs: ViewConfigs) => dispatch(setViewConfigs(configs)),
|
setViewConfigs: (configs: ViewConfigs) =>
|
||||||
|
dispatch(setViewConfigs(configs)),
|
||||||
switchFilter: (filter: FilterType) => dispatch(switchFilter(filter)),
|
switchFilter: (filter: FilterType) => dispatch(switchFilter(filter)),
|
||||||
toggleFilter: (filter: FilterType) => dispatch(toggleFilter(filter)),
|
toggleFilter: (filter: FilterType) => dispatch(toggleFilter(filter)),
|
||||||
markAllRead: (sids?: number[], date?: Date, before?: boolean) => {
|
markAllRead: (sids?: number[], date?: Date, before?: boolean) => {
|
||||||
@ -79,10 +106,10 @@ const mapDispatchToProps = dispatch => {
|
|||||||
},
|
},
|
||||||
fetchItems: (sids: number[]) => dispatch(fetchItems(false, sids)),
|
fetchItems: (sids: number[]) => dispatch(fetchItems(false, sids)),
|
||||||
settings: (sids: number[]) => dispatch(toggleSettings(true, sids)),
|
settings: (sids: number[]) => dispatch(toggleSettings(true, sids)),
|
||||||
close: () => dispatch(closeContextMenu())
|
close: () => dispatch(closeContextMenu()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const connector = connect(mapStateToProps, mapDispatchToProps)
|
const connector = connect(mapStateToProps, mapDispatchToProps)
|
||||||
export type ContextReduxProps = typeof connector
|
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 getSources = (state: RootState) => state.sources
|
||||||
const getItems = (state: RootState) => state.items
|
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 getFilter = (state: RootState) => state.page.filter
|
||||||
const getView = (_, props: FeedContainerProps) => props.viewType
|
const getView = (_, props: FeedContainerProps) => props.viewType
|
||||||
const getViewConfigs = (state: RootState) => state.page.viewConfigs
|
const getViewConfigs = (state: RootState) => state.page.viewConfigs
|
||||||
@ -23,7 +24,15 @@ const getCurrentItem = (state: RootState) => state.page.itemId
|
|||||||
|
|
||||||
const makeMapStateToProps = () => {
|
const makeMapStateToProps = () => {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
[getSources, getItems, getFeed, getView, getFilter, getViewConfigs, getCurrentItem],
|
[
|
||||||
|
getSources,
|
||||||
|
getItems,
|
||||||
|
getFeed,
|
||||||
|
getView,
|
||||||
|
getFilter,
|
||||||
|
getViewConfigs,
|
||||||
|
getCurrentItem,
|
||||||
|
],
|
||||||
(sources, items, feed, viewType, filter, viewConfigs, currentItem) => ({
|
(sources, items, feed, viewType, filter, viewConfigs, currentItem) => ({
|
||||||
feed: feed,
|
feed: feed,
|
||||||
items: feed.iids.map(iid => items[iid]),
|
items: feed.iids.map(iid => items[iid]),
|
||||||
@ -37,14 +46,16 @@ const makeMapStateToProps = () => {
|
|||||||
}
|
}
|
||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = dispatch => {
|
||||||
return {
|
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)),
|
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)),
|
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)
|
const connector = connect(makeMapStateToProps, mapDispatchToProps)
|
||||||
export type FeedReduxProps = typeof connector
|
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 mapStateToProps = createSelector(getLogs, logs => logs)
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = dispatch => {
|
||||||
return {
|
return {
|
||||||
close: () => dispatch(toggleLogMenu()),
|
close: () => dispatch(toggleLogMenu()),
|
||||||
showItem: (iid: number) => dispatch(showItemFromId(iid))
|
showItem: (iid: number) => dispatch(showItemFromId(iid)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const LogMenuContainer = connect(mapStateToProps, mapDispatchToProps)(LogMenu)
|
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 { toggleMenu, openGroupMenu } from "../scripts/models/app"
|
||||||
import { toggleGroupExpansion } from "../scripts/models/group"
|
import { toggleGroupExpansion } from "../scripts/models/group"
|
||||||
import { SourceGroup } from "../schema-types"
|
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 { ViewType } from "../schema-types"
|
||||||
import { initFeeds } from "../scripts/models/feed"
|
import { initFeeds } from "../scripts/models/feed"
|
||||||
import { RSSSource } from "../scripts/models/source"
|
import { RSSSource } from "../scripts/models/source"
|
||||||
@ -14,7 +18,8 @@ const getApp = (state: RootState) => state.app
|
|||||||
const getSources = (state: RootState) => state.sources
|
const getSources = (state: RootState) => state.sources
|
||||||
const getGroups = (state: RootState) => state.groups
|
const getGroups = (state: RootState) => state.groups
|
||||||
const getSearchOn = (state: RootState) => state.page.searchOn
|
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(
|
const mapStateToProps = createSelector(
|
||||||
[getApp, getSources, getGroups, getSearchOn, getItemOn],
|
[getApp, getSources, getGroups, getSearchOn, getItemOn],
|
||||||
@ -32,21 +37,24 @@ const mapStateToProps = createSelector(
|
|||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
toggleMenu: () => dispatch(toggleMenu()),
|
toggleMenu: () => dispatch(toggleMenu()),
|
||||||
allArticles: (init = false) => {
|
allArticles: (init = false) => {
|
||||||
dispatch(selectAllArticles(init)),
|
dispatch(selectAllArticles(init)), dispatch(initFeeds())
|
||||||
dispatch(initFeeds())
|
|
||||||
},
|
},
|
||||||
selectSourceGroup: (group: SourceGroup, menuKey: string) => {
|
selectSourceGroup: (group: SourceGroup, menuKey: string) => {
|
||||||
dispatch(selectSources(group.sids, menuKey, group.name))
|
dispatch(selectSources(group.sids, menuKey, group.name))
|
||||||
dispatch(initFeeds())
|
dispatch(initFeeds())
|
||||||
},
|
},
|
||||||
selectSource: (source: RSSSource) => {
|
selectSource: (source: RSSSource) => {
|
||||||
dispatch(selectSources([source.sid], "s-"+source.sid, source.name))
|
dispatch(selectSources([source.sid], "s-" + source.sid, source.name))
|
||||||
dispatch(initFeeds())
|
dispatch(initFeeds())
|
||||||
},
|
},
|
||||||
groupContextMenu: (sids: number[], event: React.MouseEvent) => {
|
groupContextMenu: (sids: number[], event: React.MouseEvent) => {
|
||||||
dispatch(openGroupMenu(sids, event))
|
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) {
|
if ((event.target as HTMLElement).tagName === "I" || key === selected) {
|
||||||
let [type, index] = key.split("-")
|
let [type, index] = key.split("-")
|
||||||
if (type === "g") dispatch(toggleGroupExpansion(parseInt(index)))
|
if (type === "g") dispatch(toggleGroupExpansion(parseInt(index)))
|
||||||
@ -56,4 +64,4 @@ const mapDispatchToProps = dispatch => ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const MenuContainer = connect(mapStateToProps, mapDispatchToProps)(Menu)
|
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 { createSelector } from "reselect"
|
||||||
import { RootState } from "../scripts/reducer"
|
import { RootState } from "../scripts/reducer"
|
||||||
import { fetchItems, markAllRead } from "../scripts/models/item"
|
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 { toggleSearch } from "../scripts/models/page"
|
||||||
import { ViewType } from "../schema-types"
|
import { ViewType } from "../schema-types"
|
||||||
import Nav from "../components/nav"
|
import Nav from "../components/nav"
|
||||||
|
|
||||||
const getState = (state: RootState) => state.app
|
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(
|
const mapStateToProps = createSelector(
|
||||||
[getState, getItemShown],
|
[getState, getItemShown],
|
||||||
(state, itemShown) => ({
|
(state, itemShown) => ({
|
||||||
state: state,
|
state: state,
|
||||||
itemShown: itemShown
|
itemShown: itemShown,
|
||||||
}
|
})
|
||||||
))
|
)
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
fetch: () => dispatch(fetchItems()),
|
fetch: () => dispatch(fetchItems()),
|
||||||
menu: () => dispatch(toggleMenu()),
|
menu: () => dispatch(toggleMenu()),
|
||||||
logs: () => dispatch(toggleLogMenu()),
|
logs: () => dispatch(toggleLogMenu()),
|
||||||
views: () => dispatch(openViewMenu()),
|
views: () => dispatch(openViewMenu()),
|
||||||
settings: () => dispatch(toggleSettings()),
|
settings: () => dispatch(toggleSettings()),
|
||||||
search: () => dispatch(toggleSearch()),
|
search: () => dispatch(toggleSearch()),
|
||||||
markAllRead: () => dispatch(openMarkAllMenu())
|
markAllRead: () => dispatch(openMarkAllMenu()),
|
||||||
})
|
})
|
||||||
|
|
||||||
const NavContainer = connect(mapStateToProps, mapDispatchToProps)(Nav)
|
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 getPage = (state: RootState) => state.page
|
||||||
const getSettings = (state: RootState) => state.app.settings.display
|
const getSettings = (state: RootState) => state.app.settings.display
|
||||||
const getMenu = (state: RootState) => state.app.menu
|
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(
|
const mapStateToProps = createSelector(
|
||||||
[getPage, getSettings, getMenu, getContext],
|
[getPage, getSettings, getMenu, getContext],
|
||||||
@ -20,14 +21,14 @@ const mapStateToProps = createSelector(
|
|||||||
contextOn: contextOn,
|
contextOn: contextOn,
|
||||||
itemId: page.itemId,
|
itemId: page.itemId,
|
||||||
itemFromFeed: page.itemFromFeed,
|
itemFromFeed: page.itemFromFeed,
|
||||||
viewType: page.viewType
|
viewType: page.viewType,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch: AppDispatch) => ({
|
const mapDispatchToProps = (dispatch: AppDispatch) => ({
|
||||||
dismissItem: () => dispatch(dismissItem()),
|
dismissItem: () => dispatch(dismissItem()),
|
||||||
offsetItem: (offset: number) => dispatch(showOffsetItem(offset))
|
offsetItem: (offset: number) => dispatch(showOffsetItem(offset)),
|
||||||
})
|
})
|
||||||
|
|
||||||
const PageContainer = connect(mapStateToProps, mapDispatchToProps)(Page)
|
const PageContainer = connect(mapStateToProps, mapDispatchToProps)(Page)
|
||||||
export default PageContainer
|
export default PageContainer
|
||||||
|
@ -1,24 +1,26 @@
|
|||||||
import { connect } from "react-redux"
|
import { connect } from "react-redux"
|
||||||
import { createSelector } from "reselect"
|
import { createSelector } from "reselect"
|
||||||
import { RootState } from "../scripts/reducer"
|
import { RootState } from "../scripts/reducer"
|
||||||
import { exitSettings} from "../scripts/models/app"
|
import { exitSettings } from "../scripts/models/app"
|
||||||
import Settings from "../components/settings"
|
import Settings from "../components/settings"
|
||||||
|
|
||||||
const getApp = (state: RootState) => state.app
|
const getApp = (state: RootState) => state.app
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector([getApp], app => ({
|
||||||
[getApp],
|
|
||||||
(app) => ({
|
|
||||||
display: app.settings.display,
|
display: app.settings.display,
|
||||||
blocked: !app.sourceInit || app.syncing || app.fetchingItems || app.settings.saving,
|
blocked:
|
||||||
exitting: app.settings.saving
|
!app.sourceInit ||
|
||||||
|
app.syncing ||
|
||||||
|
app.fetchingItems ||
|
||||||
|
app.settings.saving,
|
||||||
|
exitting: app.settings.saving,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = dispatch => {
|
||||||
return {
|
return {
|
||||||
close: () => dispatch(exitSettings())
|
close: () => dispatch(exitSettings()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const SettingsContainer = connect(mapStateToProps, mapDispatchToProps)(Settings)
|
const SettingsContainer = connect(mapStateToProps, mapDispatchToProps)(Settings)
|
||||||
export default SettingsContainer
|
export default SettingsContainer
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import { connect } from "react-redux"
|
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 * as db from "../../scripts/db"
|
||||||
import AppTab from "../../components/settings/app"
|
import AppTab from "../../components/settings/app"
|
||||||
import { importAll } from "../../scripts/settings"
|
import { importAll } from "../../scripts/settings"
|
||||||
@ -19,16 +23,20 @@ const mapDispatchToProps = (dispatch: AppDispatch) => ({
|
|||||||
dispatch(saveSettings())
|
dispatch(saveSettings())
|
||||||
let date = new Date()
|
let date = new Date()
|
||||||
date.setTime(date.getTime() - days * 86400000)
|
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())
|
await dispatch(updateUnreadCounts())
|
||||||
dispatch(saveSettings())
|
dispatch(saveSettings())
|
||||||
},
|
},
|
||||||
importAll: async () => {
|
importAll: async () => {
|
||||||
dispatch(saveSettings())
|
dispatch(saveSettings())
|
||||||
let cancelled = await importAll()
|
let cancelled = await importAll()
|
||||||
if (cancelled) dispatch(saveSettings())
|
if (cancelled) dispatch(saveSettings())
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const AppTabContainer = connect(null, mapDispatchToProps)(AppTab)
|
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 { createSelector } from "reselect"
|
||||||
import { RootState } from "../../scripts/reducer"
|
import { RootState } from "../../scripts/reducer"
|
||||||
import GroupsTab from "../../components/settings/groups"
|
import GroupsTab from "../../components/settings/groups"
|
||||||
import { createSourceGroup, updateSourceGroup, addSourceToGroup,
|
import {
|
||||||
deleteSourceGroup, removeSourceFromGroup, reorderSourceGroups } from "../../scripts/models/group"
|
createSourceGroup,
|
||||||
|
updateSourceGroup,
|
||||||
|
addSourceToGroup,
|
||||||
|
deleteSourceGroup,
|
||||||
|
removeSourceFromGroup,
|
||||||
|
reorderSourceGroups,
|
||||||
|
} from "../../scripts/models/group"
|
||||||
import { SourceGroup, SyncService } from "../../schema-types"
|
import { SourceGroup, SyncService } from "../../schema-types"
|
||||||
import { importGroups } from "../../scripts/models/service"
|
import { importGroups } from "../../scripts/models/service"
|
||||||
import { AppDispatch } from "../../scripts/utils"
|
import { AppDispatch } from "../../scripts/utils"
|
||||||
|
|
||||||
const getSources = (state: RootState) => state.sources
|
const getSources = (state: RootState) => state.sources
|
||||||
const getGroups = (state: RootState) => state.groups
|
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(
|
const mapStateToProps = createSelector(
|
||||||
[getSources, getGroups, getServiceOn],
|
[getSources, getGroups, getServiceOn],
|
||||||
@ -18,19 +25,26 @@ const mapStateToProps = createSelector(
|
|||||||
sources: sources,
|
sources: sources,
|
||||||
groups: groups.map((g, i) => ({ ...g, index: i })),
|
groups: groups.map((g, i) => ({ ...g, index: i })),
|
||||||
serviceOn: serviceOn,
|
serviceOn: serviceOn,
|
||||||
key: groups.length
|
key: groups.length,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch: AppDispatch) => ({
|
const mapDispatchToProps = (dispatch: AppDispatch) => ({
|
||||||
createGroup: (name: string) => dispatch(createSourceGroup(name)),
|
createGroup: (name: string) => dispatch(createSourceGroup(name)),
|
||||||
updateGroup: (group: SourceGroup) => dispatch(updateSourceGroup(group)),
|
updateGroup: (group: SourceGroup) => dispatch(updateSourceGroup(group)),
|
||||||
addToGroup: (groupIndex: number, sid: number) => dispatch(addSourceToGroup(groupIndex, sid)),
|
addToGroup: (groupIndex: number, sid: number) =>
|
||||||
deleteGroup: (groupIndex: number) => dispatch(deleteSourceGroup(groupIndex)),
|
dispatch(addSourceToGroup(groupIndex, sid)),
|
||||||
removeFromGroup: (groupIndex: number, sids: number[]) => dispatch(removeSourceFromGroup(groupIndex, sids)),
|
deleteGroup: (groupIndex: number) =>
|
||||||
reorderGroups: (groups: SourceGroup[]) => dispatch(reorderSourceGroups(groups)),
|
dispatch(deleteSourceGroup(groupIndex)),
|
||||||
|
removeFromGroup: (groupIndex: number, sids: number[]) =>
|
||||||
|
dispatch(removeSourceFromGroup(groupIndex, sids)),
|
||||||
|
reorderGroups: (groups: SourceGroup[]) =>
|
||||||
|
dispatch(reorderSourceGroups(groups)),
|
||||||
importGroups: () => dispatch(importGroups()),
|
importGroups: () => dispatch(importGroups()),
|
||||||
})
|
})
|
||||||
|
|
||||||
const GroupsTabContainer = connect(mapStateToProps, mapDispatchToProps)(GroupsTab)
|
const GroupsTabContainer = connect(
|
||||||
export default GroupsTabContainer
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(GroupsTab)
|
||||||
|
export default GroupsTabContainer
|
||||||
|
@ -8,19 +8,16 @@ import { SourceRule } from "../../scripts/models/rule"
|
|||||||
|
|
||||||
const getSources = (state: RootState) => state.sources
|
const getSources = (state: RootState) => state.sources
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector([getSources], sources => ({
|
||||||
[getSources],
|
sources: sources,
|
||||||
(sources) => ({
|
}))
|
||||||
sources: sources
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch: AppDispatch) => ({
|
const mapDispatchToProps = (dispatch: AppDispatch) => ({
|
||||||
updateSourceRules: (source: RSSSource, rules: SourceRule[]) => {
|
updateSourceRules: (source: RSSSource, rules: SourceRule[]) => {
|
||||||
source.rules = rules
|
source.rules = rules
|
||||||
dispatch(updateSource(source))
|
dispatch(updateSource(source))
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const RulesTabContainer = connect(mapStateToProps, mapDispatchToProps)(RulesTab)
|
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 { ServiceTab } from "../../components/settings/service"
|
||||||
import { AppDispatch } from "../../scripts/utils"
|
import { AppDispatch } from "../../scripts/utils"
|
||||||
import { ServiceConfigs } from "../../schema-types"
|
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"
|
import { saveSettings } from "../../scripts/models/app"
|
||||||
|
|
||||||
const getService = (state: RootState) => state.service
|
const getService = (state: RootState) => state.service
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector([getService], service => ({
|
||||||
[getService],
|
configs: service,
|
||||||
(service) => ({
|
}))
|
||||||
configs: service
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch: AppDispatch) => ({
|
const mapDispatchToProps = (dispatch: AppDispatch) => ({
|
||||||
save: (configs: ServiceConfigs) => dispatch(saveServiceConfigs(configs)),
|
save: (configs: ServiceConfigs) => dispatch(saveServiceConfigs(configs)),
|
||||||
@ -34,8 +36,11 @@ const mapDispatchToProps = (dispatch: AppDispatch) => ({
|
|||||||
console.log(err)
|
console.log(err)
|
||||||
return configs
|
return configs
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const ServiceTabContainer = connect(mapStateToProps, mapDispatchToProps)(ServiceTab)
|
const ServiceTabContainer = connect(
|
||||||
export default ServiceTabContainer
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(ServiceTab)
|
||||||
|
export default ServiceTabContainer
|
||||||
|
@ -3,14 +3,22 @@ import { connect } from "react-redux"
|
|||||||
import { createSelector } from "reselect"
|
import { createSelector } from "reselect"
|
||||||
import { RootState } from "../../scripts/reducer"
|
import { RootState } from "../../scripts/reducer"
|
||||||
import SourcesTab from "../../components/settings/sources"
|
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 { importOPML, exportOPML } from "../../scripts/models/group"
|
||||||
import { AppDispatch, validateFavicon } from "../../scripts/utils"
|
import { AppDispatch, validateFavicon } from "../../scripts/utils"
|
||||||
import { saveSettings, toggleSettings } from "../../scripts/models/app"
|
import { saveSettings, toggleSettings } from "../../scripts/models/app"
|
||||||
import { SyncService } from "../../schema-types"
|
import { SyncService } from "../../schema-types"
|
||||||
|
|
||||||
const getSources = (state: RootState) => state.sources
|
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 getSIDs = (state: RootState) => state.app.settings.sids
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
@ -23,7 +31,7 @@ const mapStateToProps = createSelector(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch: AppDispatch) => {
|
const mapDispatchToProps = (dispatch: AppDispatch) => {
|
||||||
return {
|
return {
|
||||||
acknowledgeSIDs: () => dispatch(toggleSettings(true)),
|
acknowledgeSIDs: () => dispatch(toggleSettings(true)),
|
||||||
addSource: (url: string) => dispatch(addSource(url)),
|
addSource: (url: string) => dispatch(addSource(url)),
|
||||||
updateSourceName: (source: RSSSource, name: string) => {
|
updateSourceName: (source: RSSSource, name: string) => {
|
||||||
@ -38,18 +46,32 @@ const mapDispatchToProps = (dispatch: AppDispatch) => {
|
|||||||
}
|
}
|
||||||
dispatch(saveSettings())
|
dispatch(saveSettings())
|
||||||
},
|
},
|
||||||
updateSourceOpenTarget: (source: RSSSource, target: SourceOpenTarget) => {
|
updateSourceOpenTarget: (
|
||||||
dispatch(updateSource({ ...source, openTarget: target } as RSSSource))
|
source: RSSSource,
|
||||||
|
target: SourceOpenTarget
|
||||||
|
) => {
|
||||||
|
dispatch(
|
||||||
|
updateSource({ ...source, openTarget: target } as RSSSource)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
updateFetchFrequency: (source: RSSSource, frequency: number) => {
|
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)),
|
deleteSource: (source: RSSSource) => dispatch(deleteSource(source)),
|
||||||
deleteSources: (sources: RSSSource[]) => dispatch(deleteSources(sources)),
|
deleteSources: (sources: RSSSource[]) =>
|
||||||
|
dispatch(deleteSources(sources)),
|
||||||
importOPML: () => dispatch(importOPML()),
|
importOPML: () => dispatch(importOPML()),
|
||||||
exportOPML: () => dispatch(exportOPML())
|
exportOPML: () => dispatch(exportOPML()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const SourcesTabContainer = connect(mapStateToProps, mapDispatchToProps)(SourcesTab)
|
const SourcesTabContainer = connect(
|
||||||
export default SourcesTabContainer
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(SourcesTab)
|
||||||
|
export default SourcesTabContainer
|
||||||
|
@ -12,7 +12,8 @@ if (!process.mas) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!app.isPackaged) app.setAppUserModelId(process.execPath)
|
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
|
let restarting = false
|
||||||
|
|
||||||
@ -28,29 +29,74 @@ if (process.platform === "darwin") {
|
|||||||
{
|
{
|
||||||
label: "Application",
|
label: "Application",
|
||||||
submenu: [
|
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",
|
label: "Edit",
|
||||||
submenu: [
|
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: "Cut", accelerator: "CmdOrCtrl+X", selector: "cut:" },
|
||||||
{ label: "Copy", accelerator: "CmdOrCtrl+C", selector: "copy:" },
|
{
|
||||||
{ label: "Paste", accelerator: "CmdOrCtrl+V", selector: "paste:" },
|
label: "Copy",
|
||||||
{ label: "Select All", accelerator: "CmdOrCtrl+A", selector: "selectAll:" }
|
accelerator: "CmdOrCtrl+C",
|
||||||
]
|
selector: "copy:",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Paste",
|
||||||
|
accelerator: "CmdOrCtrl+V",
|
||||||
|
selector: "paste:",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Select All",
|
||||||
|
accelerator: "CmdOrCtrl+A",
|
||||||
|
selector: "selectAll:",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Window",
|
label: "Window",
|
||||||
submenu: [
|
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: "Close",
|
||||||
{ label: "Zoom", click: () => winManager.zoom() }
|
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))
|
Menu.setApplicationMenu(Menu.buildFromTemplate(template))
|
||||||
} else {
|
} else {
|
||||||
@ -61,7 +107,9 @@ const winManager = new WindowManager()
|
|||||||
|
|
||||||
app.on("window-all-closed", () => {
|
app.on("window-all-closed", () => {
|
||||||
if (winManager.hasWindow()) {
|
if (winManager.hasWindow()) {
|
||||||
winManager.mainWindow.webContents.session.clearStorageData({ storages: ["cookies", "localstorage"] })
|
winManager.mainWindow.webContents.session.clearStorageData({
|
||||||
|
storages: ["cookies", "localstorage"],
|
||||||
|
})
|
||||||
}
|
}
|
||||||
winManager.mainWindow = null
|
winManager.mainWindow = null
|
||||||
if (restarting) {
|
if (restarting) {
|
||||||
@ -81,7 +129,10 @@ ipcMain.handle("import-all-settings", (_, configs: SchemaTypes) => {
|
|||||||
}
|
}
|
||||||
performUpdate(store)
|
performUpdate(store)
|
||||||
nativeTheme.themeSource = store.get("theme", ThemeSettings.Default)
|
nativeTheme.themeSource = store.get("theme", ThemeSettings.Default)
|
||||||
setTimeout(() => {
|
setTimeout(
|
||||||
winManager.mainWindow.close()
|
() => {
|
||||||
}, process.platform === "darwin" ? 1000 : 0); // Why ???
|
winManager.mainWindow.close()
|
||||||
|
},
|
||||||
|
process.platform === "darwin" ? 1000 : 0
|
||||||
|
) // Why ???
|
||||||
})
|
})
|
||||||
|
@ -31,4 +31,4 @@ ReactDOM.render(
|
|||||||
<Root />
|
<Root />
|
||||||
</Provider>,
|
</Provider>,
|
||||||
document.getElementById("app")
|
document.getElementById("app")
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,14 @@
|
|||||||
import Store = require("electron-store")
|
import Store = require("electron-store")
|
||||||
import { SchemaTypes, SourceGroup, ViewType, ThemeSettings, SearchEngines,
|
import {
|
||||||
SyncService, ServiceConfigs, ViewConfigs } from "../schema-types"
|
SchemaTypes,
|
||||||
|
SourceGroup,
|
||||||
|
ViewType,
|
||||||
|
ThemeSettings,
|
||||||
|
SearchEngines,
|
||||||
|
SyncService,
|
||||||
|
ServiceConfigs,
|
||||||
|
ViewConfigs,
|
||||||
|
} from "../schema-types"
|
||||||
import { ipcMain, session, nativeTheme, app } from "electron"
|
import { ipcMain, session, nativeTheme, app } from "electron"
|
||||||
import { WindowManager } from "./window"
|
import { WindowManager } from "./window"
|
||||||
|
|
||||||
@ -10,12 +18,12 @@ const GROUPS_STORE_KEY = "sourceGroups"
|
|||||||
ipcMain.handle("set-groups", (_, groups: SourceGroup[]) => {
|
ipcMain.handle("set-groups", (_, groups: SourceGroup[]) => {
|
||||||
store.set(GROUPS_STORE_KEY, groups)
|
store.set(GROUPS_STORE_KEY, groups)
|
||||||
})
|
})
|
||||||
ipcMain.on("get-groups", (event) => {
|
ipcMain.on("get-groups", event => {
|
||||||
event.returnValue = store.get(GROUPS_STORE_KEY, [])
|
event.returnValue = store.get(GROUPS_STORE_KEY, [])
|
||||||
})
|
})
|
||||||
|
|
||||||
const MENU_STORE_KEY = "menuOn"
|
const MENU_STORE_KEY = "menuOn"
|
||||||
ipcMain.on("get-menu", (event) => {
|
ipcMain.on("get-menu", event => {
|
||||||
event.returnValue = store.get(MENU_STORE_KEY, false)
|
event.returnValue = store.get(MENU_STORE_KEY, false)
|
||||||
})
|
})
|
||||||
ipcMain.handle("set-menu", (_, state: boolean) => {
|
ipcMain.handle("set-menu", (_, state: boolean) => {
|
||||||
@ -46,13 +54,13 @@ function setProxy(address = null) {
|
|||||||
session.fromPartition("sandbox").setProxy(rules)
|
session.fromPartition("sandbox").setProxy(rules)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ipcMain.on("get-proxy-status", (event) => {
|
ipcMain.on("get-proxy-status", event => {
|
||||||
event.returnValue = getProxyStatus()
|
event.returnValue = getProxyStatus()
|
||||||
})
|
})
|
||||||
ipcMain.on("toggle-proxy-status", () => {
|
ipcMain.on("toggle-proxy-status", () => {
|
||||||
toggleProxyStatus()
|
toggleProxyStatus()
|
||||||
})
|
})
|
||||||
ipcMain.on("get-proxy", (event) => {
|
ipcMain.on("get-proxy", event => {
|
||||||
event.returnValue = getProxy()
|
event.returnValue = getProxy()
|
||||||
})
|
})
|
||||||
ipcMain.handle("set-proxy", (_, address = null) => {
|
ipcMain.handle("set-proxy", (_, address = null) => {
|
||||||
@ -60,7 +68,7 @@ ipcMain.handle("set-proxy", (_, address = null) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const VIEW_STORE_KEY = "view"
|
const VIEW_STORE_KEY = "view"
|
||||||
ipcMain.on("get-view", (event) => {
|
ipcMain.on("get-view", event => {
|
||||||
event.returnValue = store.get(VIEW_STORE_KEY, ViewType.Cards)
|
event.returnValue = store.get(VIEW_STORE_KEY, ViewType.Cards)
|
||||||
})
|
})
|
||||||
ipcMain.handle("set-view", (_, viewType: ViewType) => {
|
ipcMain.handle("set-view", (_, viewType: ViewType) => {
|
||||||
@ -68,14 +76,14 @@ ipcMain.handle("set-view", (_, viewType: ViewType) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const THEME_STORE_KEY = "theme"
|
const THEME_STORE_KEY = "theme"
|
||||||
ipcMain.on("get-theme", (event) => {
|
ipcMain.on("get-theme", event => {
|
||||||
event.returnValue = store.get(THEME_STORE_KEY, ThemeSettings.Default)
|
event.returnValue = store.get(THEME_STORE_KEY, ThemeSettings.Default)
|
||||||
})
|
})
|
||||||
ipcMain.handle("set-theme", (_, theme: ThemeSettings) => {
|
ipcMain.handle("set-theme", (_, theme: ThemeSettings) => {
|
||||||
store.set(THEME_STORE_KEY, theme)
|
store.set(THEME_STORE_KEY, theme)
|
||||||
nativeTheme.themeSource = theme
|
nativeTheme.themeSource = theme
|
||||||
})
|
})
|
||||||
ipcMain.on("get-theme-dark-color", (event) => {
|
ipcMain.on("get-theme-dark-color", event => {
|
||||||
event.returnValue = nativeTheme.shouldUseDarkColors
|
event.returnValue = nativeTheme.shouldUseDarkColors
|
||||||
})
|
})
|
||||||
export function setThemeListener(manager: WindowManager) {
|
export function setThemeListener(manager: WindowManager) {
|
||||||
@ -97,24 +105,24 @@ ipcMain.handle("set-locale", (_, option: string) => {
|
|||||||
function getLocaleSettings() {
|
function getLocaleSettings() {
|
||||||
return store.get(LOCALE_STORE_KEY, "default")
|
return store.get(LOCALE_STORE_KEY, "default")
|
||||||
}
|
}
|
||||||
ipcMain.on("get-locale-settings", (event) => {
|
ipcMain.on("get-locale-settings", event => {
|
||||||
event.returnValue = getLocaleSettings()
|
event.returnValue = getLocaleSettings()
|
||||||
})
|
})
|
||||||
ipcMain.on("get-locale", (event) => {
|
ipcMain.on("get-locale", event => {
|
||||||
let setting = getLocaleSettings()
|
let setting = getLocaleSettings()
|
||||||
let locale = setting === "default" ? app.getLocale() : setting
|
let locale = setting === "default" ? app.getLocale() : setting
|
||||||
event.returnValue = locale
|
event.returnValue = locale
|
||||||
})
|
})
|
||||||
|
|
||||||
const FONT_SIZE_STORE_KEY = "fontSize"
|
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)
|
event.returnValue = store.get(FONT_SIZE_STORE_KEY, 16)
|
||||||
})
|
})
|
||||||
ipcMain.handle("set-font-size", (_, size: number) => {
|
ipcMain.handle("set-font-size", (_, size: number) => {
|
||||||
store.set(FONT_SIZE_STORE_KEY, size)
|
store.set(FONT_SIZE_STORE_KEY, size)
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on("get-all-settings", (event) => {
|
ipcMain.on("get-all-settings", event => {
|
||||||
let output = {}
|
let output = {}
|
||||||
for (let [key, value] of store) {
|
for (let [key, value] of store) {
|
||||||
output[key] = value
|
output[key] = value
|
||||||
@ -123,7 +131,7 @@ ipcMain.on("get-all-settings", (event) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const FETCH_INTEVAL_STORE_KEY = "fetchInterval"
|
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)
|
event.returnValue = store.get(FETCH_INTEVAL_STORE_KEY, 0)
|
||||||
})
|
})
|
||||||
ipcMain.handle("set-fetch-interval", (_, interval: number) => {
|
ipcMain.handle("set-fetch-interval", (_, interval: number) => {
|
||||||
@ -131,7 +139,7 @@ ipcMain.handle("set-fetch-interval", (_, interval: number) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const SEARCH_ENGINE_STORE_KEY = "searchEngine"
|
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)
|
event.returnValue = store.get(SEARCH_ENGINE_STORE_KEY, SearchEngines.Google)
|
||||||
})
|
})
|
||||||
ipcMain.handle("set-search-engine", (_, engine: SearchEngines) => {
|
ipcMain.handle("set-search-engine", (_, engine: SearchEngines) => {
|
||||||
@ -139,15 +147,17 @@ ipcMain.handle("set-search-engine", (_, engine: SearchEngines) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const SERVICE_CONFIGS_STORE_KEY = "serviceConfigs"
|
const SERVICE_CONFIGS_STORE_KEY = "serviceConfigs"
|
||||||
ipcMain.on("get-service-configs", (event) => {
|
ipcMain.on("get-service-configs", event => {
|
||||||
event.returnValue = store.get(SERVICE_CONFIGS_STORE_KEY, { type: SyncService.None })
|
event.returnValue = store.get(SERVICE_CONFIGS_STORE_KEY, {
|
||||||
|
type: SyncService.None,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
ipcMain.handle("set-service-configs", (_, configs: ServiceConfigs) => {
|
ipcMain.handle("set-service-configs", (_, configs: ServiceConfigs) => {
|
||||||
store.set(SERVICE_CONFIGS_STORE_KEY, configs)
|
store.set(SERVICE_CONFIGS_STORE_KEY, configs)
|
||||||
})
|
})
|
||||||
|
|
||||||
const FILTER_TYPE_STORE_KEY = "filterType"
|
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)
|
event.returnValue = store.get(FILTER_TYPE_STORE_KEY, null)
|
||||||
})
|
})
|
||||||
ipcMain.handle("set-filter-type", (_, filterType: number) => {
|
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) => {
|
ipcMain.on("get-view-configs", (event, view: ViewType) => {
|
||||||
switch (view) {
|
switch (view) {
|
||||||
case ViewType.List:
|
case ViewType.List:
|
||||||
event.returnValue = store.get(LIST_CONFIGS_STORE_KEY, ViewConfigs.ShowCover)
|
event.returnValue = store.get(
|
||||||
|
LIST_CONFIGS_STORE_KEY,
|
||||||
|
ViewConfigs.ShowCover
|
||||||
|
)
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
event.returnValue = undefined
|
event.returnValue = undefined
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
ipcMain.handle("set-view-configs", (_, view: ViewType, configs: ViewConfigs) => {
|
ipcMain.handle(
|
||||||
switch (view) {
|
"set-view-configs",
|
||||||
case ViewType.List:
|
(_, view: ViewType, configs: ViewConfigs) => {
|
||||||
store.set(LIST_CONFIGS_STORE_KEY, configs)
|
switch (view) {
|
||||||
break
|
case ViewType.List:
|
||||||
|
store.set(LIST_CONFIGS_STORE_KEY, configs)
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
const NEDB_STATUS_STORE_KEY = "useNeDB"
|
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)
|
event.returnValue = store.get(NEDB_STATUS_STORE_KEY, true)
|
||||||
})
|
})
|
||||||
ipcMain.handle("set-nedb-status", (_, flag: boolean) => {
|
ipcMain.handle("set-nedb-status", (_, flag: boolean) => {
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import { TouchBarTexts } from "../schema-types"
|
import { TouchBarTexts } from "../schema-types"
|
||||||
import { BrowserWindow, TouchBar } from "electron"
|
import { BrowserWindow, TouchBar } from "electron"
|
||||||
|
|
||||||
|
function createTouchBarFunctionButton(
|
||||||
function createTouchBarFunctionButton(window: BrowserWindow, text: string, key: string) {
|
window: BrowserWindow,
|
||||||
|
text: string,
|
||||||
|
key: string
|
||||||
|
) {
|
||||||
return new TouchBar.TouchBarButton({
|
return new TouchBar.TouchBarButton({
|
||||||
label: text,
|
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.refresh, "F5"),
|
||||||
createTouchBarFunctionButton(window, texts.markAll, "F6"),
|
createTouchBarFunctionButton(window, texts.markAll, "F6"),
|
||||||
createTouchBarFunctionButton(window, texts.notifications, "F7"),
|
createTouchBarFunctionButton(window, texts.notifications, "F7"),
|
||||||
]
|
],
|
||||||
})
|
})
|
||||||
window.setTouchBar(touchBar)
|
window.setTouchBar(touchBar)
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,10 @@ export default function performUpdate(store: Store<SchemaTypes>) {
|
|||||||
if (useNeDB === undefined) {
|
if (useNeDB === undefined) {
|
||||||
if (version !== null) {
|
if (version !== null) {
|
||||||
const revs = version.split(".").map(s => parseInt(s))
|
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 {
|
} else {
|
||||||
store.set("useNeDB", false)
|
store.set("useNeDB", false)
|
||||||
}
|
}
|
||||||
@ -18,4 +21,4 @@ export default function performUpdate(store: Store<SchemaTypes>) {
|
|||||||
if (version != currentVersion) {
|
if (version != currentVersion) {
|
||||||
store.set("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 { WindowManager } from "./window"
|
||||||
import fs = require("fs")
|
import fs = require("fs")
|
||||||
import { ImageCallbackTypes, TouchBarTexts } from "../schema-types"
|
import { ImageCallbackTypes, TouchBarTexts } from "../schema-types"
|
||||||
import { initMainTouchBar } from "./touchbar"
|
import { initMainTouchBar } from "./touchbar"
|
||||||
|
|
||||||
export function setUtilsListeners(manager: WindowManager) {
|
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 (url.startsWith("https://") || url.startsWith("http://")) {
|
||||||
if (background && process.platform === "darwin") {
|
if (background && process.platform === "darwin") {
|
||||||
shell.openExternal(url, { activate: false })
|
shell.openExternal(url, { activate: false })
|
||||||
@ -18,21 +26,21 @@ export function setUtilsListeners(manager: WindowManager) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app.on("web-contents-created", (_, contents) => {
|
app.on("web-contents-created", (_, contents) => {
|
||||||
// TODO: Use contents.setWindowOpenHandler instead of new-window listener
|
// TODO: Use contents.setWindowOpenHandler instead of new-window listener
|
||||||
contents.on("new-window", (event, url, _, disposition) => {
|
contents.on("new-window", (event, url, _, disposition) => {
|
||||||
if (manager.hasWindow()) event.preventDefault()
|
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) => {
|
contents.on("will-navigate", (event, url) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
if (contents.getType() === "webview") openExternal(url)
|
if (contents.getType() === "webview") openExternal(url)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
ipcMain.on("get-version", (event) => {
|
ipcMain.on("get-version", event => {
|
||||||
event.returnValue = app.getVersion()
|
event.returnValue = app.getVersion()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -44,57 +52,76 @@ export function setUtilsListeners(manager: WindowManager) {
|
|||||||
dialog.showErrorBox(title, content)
|
dialog.showErrorBox(title, content)
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle("show-message-box", async (_, title, message, confirm, cancel, defaultCancel, type) => {
|
ipcMain.handle(
|
||||||
if (manager.hasWindow()) {
|
"show-message-box",
|
||||||
let response = await dialog.showMessageBox(manager.mainWindow, {
|
async (_, title, message, confirm, cancel, defaultCancel, type) => {
|
||||||
type: type,
|
if (manager.hasWindow()) {
|
||||||
title: title,
|
let response = await dialog.showMessageBox(manager.mainWindow, {
|
||||||
message: message,
|
type: type,
|
||||||
buttons: process.platform === "win32" ? ["Yes", "No"] : [confirm, cancel],
|
title: title,
|
||||||
cancelId: 1,
|
message: message,
|
||||||
defaultId: defaultCancel ? 1 : 0
|
buttons:
|
||||||
})
|
process.platform === "win32"
|
||||||
return response.response === 0
|
? ["Yes", "No"]
|
||||||
} else {
|
: [confirm, cancel],
|
||||||
return false
|
cancelId: 1,
|
||||||
}
|
defaultId: defaultCancel ? 1 : 0,
|
||||||
})
|
|
||||||
|
|
||||||
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 response.response === 0
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
)
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.handle("show-open-dialog", async (_, filters: Electron.FileFilter[]) => {
|
ipcMain.handle(
|
||||||
if (manager.hasWindow()) {
|
"show-save-dialog",
|
||||||
let response = await dialog.showOpenDialog(manager.mainWindow, {
|
async (_, filters: Electron.FileFilter[], path: string) => {
|
||||||
filters: filters,
|
ipcMain.removeAllListeners("write-save-result")
|
||||||
properties: ["openFile"]
|
if (manager.hasWindow()) {
|
||||||
})
|
let response = await dialog.showSaveDialog(manager.mainWindow, {
|
||||||
if (!response.canceled) {
|
defaultPath: path,
|
||||||
try {
|
filters: filters,
|
||||||
return await fs.promises.readFile(response.filePaths[0], "utf-8")
|
})
|
||||||
} catch (err) {
|
if (!response.canceled) {
|
||||||
console.log(err)
|
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 () => {
|
ipcMain.handle("get-cache", async () => {
|
||||||
return await session.defaultSession.getCacheSize()
|
return await session.defaultSession.getCacheSize()
|
||||||
@ -106,37 +133,67 @@ export function setUtilsListeners(manager: WindowManager) {
|
|||||||
|
|
||||||
app.on("web-contents-created", (_, contents) => {
|
app.on("web-contents-created", (_, contents) => {
|
||||||
if (contents.getType() === "webview") {
|
if (contents.getType() === "webview") {
|
||||||
contents.on("did-fail-load", (event, code, desc, validated, isMainFrame) => {
|
contents.on(
|
||||||
if (isMainFrame && manager.hasWindow()) {
|
"did-fail-load",
|
||||||
manager.mainWindow.webContents.send("webview-error", desc)
|
(event, code, desc, validated, isMainFrame) => {
|
||||||
|
if (isMainFrame && manager.hasWindow()) {
|
||||||
|
manager.mainWindow.webContents.send(
|
||||||
|
"webview-error",
|
||||||
|
desc
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
contents.on("context-menu", (_, params) => {
|
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) {
|
if (params.hasImageContents) {
|
||||||
ipcMain.removeHandler("image-callback")
|
ipcMain.removeHandler("image-callback")
|
||||||
ipcMain.handleOnce("image-callback", (_, type: ImageCallbackTypes) => {
|
ipcMain.handleOnce(
|
||||||
switch (type) {
|
"image-callback",
|
||||||
case ImageCallbackTypes.OpenExternal:
|
(_, type: ImageCallbackTypes) => {
|
||||||
case ImageCallbackTypes.OpenExternalBg:
|
switch (type) {
|
||||||
openExternal(params.srcURL, type === ImageCallbackTypes.OpenExternalBg)
|
case ImageCallbackTypes.OpenExternal:
|
||||||
break
|
case ImageCallbackTypes.OpenExternalBg:
|
||||||
case ImageCallbackTypes.SaveAs:
|
openExternal(
|
||||||
contents.session.downloadURL(params.srcURL)
|
params.srcURL,
|
||||||
break
|
type ===
|
||||||
case ImageCallbackTypes.Copy:
|
ImageCallbackTypes.OpenExternalBg
|
||||||
contents.copyImageAt(params.x, params.y)
|
)
|
||||||
break
|
break
|
||||||
case ImageCallbackTypes.CopyLink:
|
case ImageCallbackTypes.SaveAs:
|
||||||
clipboard.writeText(params.srcURL)
|
contents.session.downloadURL(
|
||||||
break
|
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 {
|
} 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 = () => {
|
const dismiss = () => {
|
||||||
document.removeEventListener("mousedown", dismiss)
|
document.removeEventListener("mousedown", dismiss)
|
||||||
document.removeEventListener("scroll", dismiss)
|
document.removeEventListener("scroll", dismiss)
|
||||||
@ -144,11 +201,15 @@ export function setUtilsListeners(manager: WindowManager) {
|
|||||||
}
|
}
|
||||||
document.addEventListener("mousedown", dismiss)
|
document.addEventListener("mousedown", dismiss)
|
||||||
document.addEventListener("scroll", 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) => {
|
contents.on("before-input-event", (_, input) => {
|
||||||
@ -176,23 +237,26 @@ export function setUtilsListeners(manager: WindowManager) {
|
|||||||
manager.zoom()
|
manager.zoom()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on("is-maximized", (event) => {
|
ipcMain.on("is-maximized", event => {
|
||||||
event.returnValue = Boolean(manager.mainWindow) && manager.mainWindow.isMaximized()
|
event.returnValue =
|
||||||
|
Boolean(manager.mainWindow) && manager.mainWindow.isMaximized()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on("is-focused", (event) => {
|
ipcMain.on("is-focused", event => {
|
||||||
event.returnValue = manager.hasWindow() && manager.mainWindow.isFocused()
|
event.returnValue =
|
||||||
|
manager.hasWindow() && manager.mainWindow.isFocused()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on("is-fullscreen", (event) => {
|
ipcMain.on("is-fullscreen", event => {
|
||||||
event.returnValue = manager.hasWindow() && manager.mainWindow.isFullScreen()
|
event.returnValue =
|
||||||
|
manager.hasWindow() && manager.mainWindow.isFullScreen()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle("request-focus", () => {
|
ipcMain.handle("request-focus", () => {
|
||||||
if (manager.hasWindow()) {
|
if (manager.hasWindow()) {
|
||||||
const win = manager.mainWindow
|
const win = manager.mainWindow
|
||||||
if (win.isMinimized()) win.restore()
|
if (win.isMinimized()) win.restore()
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
win.setAlwaysOnTop(true)
|
win.setAlwaysOnTop(true)
|
||||||
win.setAlwaysOnTop(false)
|
win.setAlwaysOnTop(false)
|
||||||
}
|
}
|
||||||
@ -219,4 +283,4 @@ export function setUtilsListeners(manager: WindowManager) {
|
|||||||
ipcMain.handle("touchbar-destroy", () => {
|
ipcMain.handle("touchbar-destroy", () => {
|
||||||
if (manager.hasWindow()) manager.mainWindow.setTouchBar(null)
|
if (manager.hasWindow()) manager.mainWindow.setTouchBar(null)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ export class WindowManager {
|
|||||||
this.mainWindow.focus()
|
this.mainWindow.focus()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on("activate", () => {
|
app.on("activate", () => {
|
||||||
if (this.mainWindow === null) {
|
if (this.mainWindow === null) {
|
||||||
this.createWindow()
|
this.createWindow()
|
||||||
@ -44,7 +44,12 @@ export class WindowManager {
|
|||||||
if (!this.hasWindow()) {
|
if (!this.hasWindow()) {
|
||||||
this.mainWindow = new BrowserWindow({
|
this.mainWindow = new BrowserWindow({
|
||||||
title: "Fluent Reader",
|
title: "Fluent Reader",
|
||||||
backgroundColor: process.platform === "darwin" ? "#00000000" : (nativeTheme.shouldUseDarkColors ? "#282828" : "#faf9f8"),
|
backgroundColor:
|
||||||
|
process.platform === "darwin"
|
||||||
|
? "#00000000"
|
||||||
|
: nativeTheme.shouldUseDarkColors
|
||||||
|
? "#282828"
|
||||||
|
: "#faf9f8",
|
||||||
vibrancy: "sidebar",
|
vibrancy: "sidebar",
|
||||||
x: this.mainWindowState.x,
|
x: this.mainWindowState.x,
|
||||||
y: this.mainWindowState.y,
|
y: this.mainWindowState.y,
|
||||||
@ -62,8 +67,11 @@ export class WindowManager {
|
|||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
worldSafeExecuteJavaScript: true,
|
worldSafeExecuteJavaScript: true,
|
||||||
spellcheck: false,
|
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.mainWindowState.manage(this.mainWindow)
|
||||||
this.mainWindow.on("ready-to-show", () => {
|
this.mainWindow.on("ready-to-show", () => {
|
||||||
@ -71,7 +79,9 @@ export class WindowManager {
|
|||||||
this.mainWindow.focus()
|
this.mainWindow.focus()
|
||||||
if (!app.isPackaged) this.mainWindow.webContents.openDevTools()
|
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.on("maximize", () => {
|
||||||
this.mainWindow.webContents.send("maximized")
|
this.mainWindow.webContents.send("maximized")
|
||||||
@ -93,7 +103,11 @@ export class WindowManager {
|
|||||||
})
|
})
|
||||||
this.mainWindow.webContents.on("context-menu", (_, params) => {
|
this.mainWindow.webContents.on("context-menu", (_, params) => {
|
||||||
if (params.selectionText) {
|
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 = () => {
|
hasWindow = () => {
|
||||||
return this.mainWindow !== null && !this.mainWindow.isDestroyed()
|
return this.mainWindow !== null && !this.mainWindow.isDestroyed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,11 @@ export class SourceGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const enum ViewType {
|
export const enum ViewType {
|
||||||
Cards, List, Magazine, Compact, Customized
|
Cards,
|
||||||
|
List,
|
||||||
|
Magazine,
|
||||||
|
Compact,
|
||||||
|
Customized,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum ViewConfigs {
|
export const enum ViewConfigs {
|
||||||
@ -29,21 +33,32 @@ export const enum ViewConfigs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const enum ThemeSettings {
|
export const enum ThemeSettings {
|
||||||
Default = "system",
|
Default = "system",
|
||||||
Light = "light",
|
Light = "light",
|
||||||
Dark = "dark"
|
Dark = "dark",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum SearchEngines {
|
export const enum SearchEngines {
|
||||||
Google, Bing, Baidu, DuckDuckGo
|
Google,
|
||||||
|
Bing,
|
||||||
|
Baidu,
|
||||||
|
DuckDuckGo,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum ImageCallbackTypes {
|
export const enum ImageCallbackTypes {
|
||||||
OpenExternal, OpenExternalBg, SaveAs, Copy, CopyLink
|
OpenExternal,
|
||||||
|
OpenExternalBg,
|
||||||
|
SaveAs,
|
||||||
|
Copy,
|
||||||
|
CopyLink,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum SyncService {
|
export const enum SyncService {
|
||||||
None, Fever, Feedbin, GReader, Inoreader
|
None,
|
||||||
|
Fever,
|
||||||
|
Feedbin,
|
||||||
|
GReader,
|
||||||
|
Inoreader,
|
||||||
}
|
}
|
||||||
export interface ServiceConfigs {
|
export interface ServiceConfigs {
|
||||||
type: SyncService
|
type: SyncService
|
||||||
@ -51,7 +66,9 @@ export interface ServiceConfigs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const enum WindowStateListenerType {
|
export const enum WindowStateListenerType {
|
||||||
Maximized, Focused, Fullscreen
|
Maximized,
|
||||||
|
Focused,
|
||||||
|
Fullscreen,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TouchBarTexts {
|
export interface TouchBarTexts {
|
||||||
|
@ -5,39 +5,43 @@ import { RSSSource } from "./models/source"
|
|||||||
import { RSSItem } from "./models/item"
|
import { RSSItem } from "./models/item"
|
||||||
|
|
||||||
const sdbSchema = lf.schema.create("sourcesDB", 1)
|
const sdbSchema = lf.schema.create("sourcesDB", 1)
|
||||||
sdbSchema.createTable("sources").
|
sdbSchema
|
||||||
addColumn("sid", lf.Type.INTEGER).addPrimaryKey(["sid"], false).
|
.createTable("sources")
|
||||||
addColumn("url", lf.Type.STRING).
|
.addColumn("sid", lf.Type.INTEGER)
|
||||||
addColumn("iconurl", lf.Type.STRING).
|
.addPrimaryKey(["sid"], false)
|
||||||
addColumn("name", lf.Type.STRING).
|
.addColumn("url", lf.Type.STRING)
|
||||||
addColumn("openTarget", lf.Type.NUMBER).
|
.addColumn("iconurl", lf.Type.STRING)
|
||||||
addColumn("lastFetched", lf.Type.DATE_TIME).
|
.addColumn("name", lf.Type.STRING)
|
||||||
addColumn("serviceRef", lf.Type.STRING).
|
.addColumn("openTarget", lf.Type.NUMBER)
|
||||||
addColumn("fetchFrequency", lf.Type.NUMBER).
|
.addColumn("lastFetched", lf.Type.DATE_TIME)
|
||||||
addColumn("rules", lf.Type.OBJECT).
|
.addColumn("serviceRef", lf.Type.STRING)
|
||||||
addNullable(["iconurl", "serviceRef", "rules"]).
|
.addColumn("fetchFrequency", lf.Type.NUMBER)
|
||||||
addIndex("idxURL", ["url"], true)
|
.addColumn("rules", lf.Type.OBJECT)
|
||||||
|
.addNullable(["iconurl", "serviceRef", "rules"])
|
||||||
|
.addIndex("idxURL", ["url"], true)
|
||||||
|
|
||||||
const idbSchema = lf.schema.create("itemsDB", 1)
|
const idbSchema = lf.schema.create("itemsDB", 1)
|
||||||
idbSchema.createTable("items").
|
idbSchema
|
||||||
addColumn("_id", lf.Type.INTEGER).addPrimaryKey(["_id"], true).
|
.createTable("items")
|
||||||
addColumn("source", lf.Type.INTEGER).
|
.addColumn("_id", lf.Type.INTEGER)
|
||||||
addColumn("title", lf.Type.STRING).
|
.addPrimaryKey(["_id"], true)
|
||||||
addColumn("link", lf.Type.STRING).
|
.addColumn("source", lf.Type.INTEGER)
|
||||||
addColumn("date", lf.Type.DATE_TIME).
|
.addColumn("title", lf.Type.STRING)
|
||||||
addColumn("fetchedDate", lf.Type.DATE_TIME).
|
.addColumn("link", lf.Type.STRING)
|
||||||
addColumn("thumb", lf.Type.STRING).
|
.addColumn("date", lf.Type.DATE_TIME)
|
||||||
addColumn("content", lf.Type.STRING).
|
.addColumn("fetchedDate", lf.Type.DATE_TIME)
|
||||||
addColumn("snippet", lf.Type.STRING).
|
.addColumn("thumb", lf.Type.STRING)
|
||||||
addColumn("creator", lf.Type.STRING).
|
.addColumn("content", lf.Type.STRING)
|
||||||
addColumn("hasRead", lf.Type.BOOLEAN).
|
.addColumn("snippet", lf.Type.STRING)
|
||||||
addColumn("starred", lf.Type.BOOLEAN).
|
.addColumn("creator", lf.Type.STRING)
|
||||||
addColumn("hidden", lf.Type.BOOLEAN).
|
.addColumn("hasRead", lf.Type.BOOLEAN)
|
||||||
addColumn("notify", lf.Type.BOOLEAN).
|
.addColumn("starred", lf.Type.BOOLEAN)
|
||||||
addColumn("serviceRef", lf.Type.STRING).
|
.addColumn("hidden", lf.Type.BOOLEAN)
|
||||||
addNullable(["thumb", "creator", "serviceRef"]).
|
.addColumn("notify", lf.Type.BOOLEAN)
|
||||||
addIndex("idxDate", ["date"], false, lf.Order.DESC).
|
.addColumn("serviceRef", lf.Type.STRING)
|
||||||
addIndex("idxService", ["serviceRef"], false)
|
.addNullable(["thumb", "creator", "serviceRef"])
|
||||||
|
.addIndex("idxDate", ["date"], false, lf.Order.DESC)
|
||||||
|
.addIndex("idxService", ["serviceRef"], false)
|
||||||
|
|
||||||
export let sourcesDB: lf.Database
|
export let sourcesDB: lf.Database
|
||||||
export let sources: lf.schema.Table
|
export let sources: lf.schema.Table
|
||||||
@ -59,16 +63,16 @@ async function migrateNeDB() {
|
|||||||
const sdb = new Datastore<RSSSource>({
|
const sdb = new Datastore<RSSSource>({
|
||||||
filename: "sources",
|
filename: "sources",
|
||||||
autoload: true,
|
autoload: true,
|
||||||
onload: (err) => {
|
onload: err => {
|
||||||
if (err) window.console.log(err)
|
if (err) window.console.log(err)
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
const idb = new Datastore<RSSItem>({
|
const idb = new Datastore<RSSItem>({
|
||||||
filename: "items",
|
filename: "items",
|
||||||
autoload: true,
|
autoload: true,
|
||||||
onload: (err) => {
|
onload: err => {
|
||||||
if (err) window.console.log(err)
|
if (err) window.console.log(err)
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
const sourceDocs = await new Promise<RSSSource[]>(resolve => {
|
const sourceDocs = await new Promise<RSSSource[]>(resolve => {
|
||||||
sdb.find({}, (_, docs) => {
|
sdb.find({}, (_, docs) => {
|
||||||
@ -81,14 +85,16 @@ async function migrateNeDB() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
const sRows = sourceDocs.map(doc => {
|
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
|
// @ts-ignore
|
||||||
delete doc._id
|
delete doc._id
|
||||||
if (!doc.fetchFrequency) doc.fetchFrequency = 0
|
if (!doc.fetchFrequency) doc.fetchFrequency = 0
|
||||||
return sources.createRow(doc)
|
return sources.createRow(doc)
|
||||||
})
|
})
|
||||||
const iRows = itemDocs.map(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.title) doc.title = intl.get("article.untitled")
|
||||||
if (!doc.content) doc.content = ""
|
if (!doc.content) doc.content = ""
|
||||||
if (!doc.snippet) doc.snippet = ""
|
if (!doc.snippet) doc.snippet = ""
|
||||||
@ -100,13 +106,20 @@ async function migrateNeDB() {
|
|||||||
})
|
})
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
sourcesDB.insert().into(sources).values(sRows).exec(),
|
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)
|
window.settings.setNeDBStatus(false)
|
||||||
sdb.remove({}, { multi: true }, () => { sdb.persistence.compactDatafile() })
|
sdb.remove({}, { multi: true }, () => {
|
||||||
idb.remove({}, { multi: true }, () => { idb.persistence.compactDatafile() })
|
sdb.persistence.compactDatafile()
|
||||||
|
})
|
||||||
|
idb.remove({}, { multi: true }, () => {
|
||||||
|
idb.persistence.compactDatafile()
|
||||||
|
})
|
||||||
} catch (err) {
|
} 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()
|
window.utils.closeWindow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -224,4 +224,4 @@
|
|||||||
"fetchInterval": "Interval zwischen dem Abrufen der Daten",
|
"fetchInterval": "Interval zwischen dem Abrufen der Daten",
|
||||||
"never": "Nie"
|
"never": "Nie"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -232,4 +232,4 @@
|
|||||||
"fetchInterval": "Automatic fetch interval",
|
"fetchInterval": "Automatic fetch interval",
|
||||||
"never": "Never"
|
"never": "Never"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,235 +1,235 @@
|
|||||||
{
|
{
|
||||||
"add": "Lisää",
|
"add": "Lisää",
|
||||||
"allArticles": "Kaikki artikkelit",
|
"allArticles": "Kaikki artikkelit",
|
||||||
"app": {
|
"app": {
|
||||||
"backup": "Varmuuskopiointi",
|
"backup": "Varmuuskopiointi",
|
||||||
"badUrl": "Virheellinen URL",
|
"badUrl": "Virheellinen URL",
|
||||||
"cache": "Tyhjennä välimuisti",
|
"cache": "Tyhjennä välimuisti",
|
||||||
"cacheSize": "Välimuistissa on {size} dataa",
|
"cacheSize": "Välimuistissa on {size} dataa",
|
||||||
"calculatingSize": "Lasketaan kokoa...",
|
"calculatingSize": "Lasketaan kokoa...",
|
||||||
"cleanup": "Siivoa",
|
"cleanup": "Siivoa",
|
||||||
"confirmDelete": "Poista",
|
"confirmDelete": "Poista",
|
||||||
"confirmImport": "Haluatko tuoda tiedot varmuuskopiosta? Kaikki nykyinen data poistetaan.",
|
"confirmImport": "Haluatko tuoda tiedot varmuuskopiosta? Kaikki nykyinen data poistetaan.",
|
||||||
"darkTheme": "Tumma tila",
|
"darkTheme": "Tumma tila",
|
||||||
"data": "Sovelluksen Data",
|
"data": "Sovelluksen Data",
|
||||||
"daysAgo": "{days, plural, =1 {# päivä} other {# päivää}} sitten",
|
"daysAgo": "{days, plural, =1 {# päivä} other {# päivää}} sitten",
|
||||||
"deleteAll": "Poista kaikki artikkelit",
|
"deleteAll": "Poista kaikki artikkelit",
|
||||||
"deleteChoices": "Poista ... päivää vanhemmat artikkelit",
|
"deleteChoices": "Poista ... päivää vanhemmat artikkelit",
|
||||||
"enableProxy": "Käytä välityspalvelinta",
|
"enableProxy": "Käytä välityspalvelinta",
|
||||||
"fetchInterval": "Automaattinen päivitys",
|
"fetchInterval": "Automaattinen päivitys",
|
||||||
"frData": "Fluent Reader Data",
|
"frData": "Fluent Reader Data",
|
||||||
"itemSize": "Artikkelit käyttävät {size} paikallista tallennustilaa",
|
"itemSize": "Artikkelit käyttävät {size} paikallista tallennustilaa",
|
||||||
"language": "Näyttökieli",
|
"language": "Näyttökieli",
|
||||||
"lightTheme": "Vaalea tila",
|
"lightTheme": "Vaalea tila",
|
||||||
"never": "Ei koskaan",
|
"never": "Ei koskaan",
|
||||||
"pac": "PAC Osoite",
|
"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,",
|
"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",
|
"restore": "Palauta",
|
||||||
"setPac": "Aseta PAC",
|
"setPac": "Aseta PAC",
|
||||||
"theme": "Teema"
|
"theme": "Teema"
|
||||||
},
|
},
|
||||||
"article": {
|
"article": {
|
||||||
"dontNotify": "Älä ilmoita",
|
"dontNotify": "Älä ilmoita",
|
||||||
"empty": "Ei artikkeleita",
|
"empty": "Ei artikkeleita",
|
||||||
"error": "Artikkelin lataaminen epäonnistui.",
|
"error": "Artikkelin lataaminen epäonnistui.",
|
||||||
"fontSize": "Fonttikoko",
|
"fontSize": "Fonttikoko",
|
||||||
"hide": "Piilota artikkeli",
|
"hide": "Piilota artikkeli",
|
||||||
"loadFull": "Lataa koko sisältö",
|
"loadFull": "Lataa koko sisältö",
|
||||||
"loadWebpage": "Lataa verkkosivu",
|
"loadWebpage": "Lataa verkkosivu",
|
||||||
"markAbove": "Merkitse ylemmät luetuksi",
|
"markAbove": "Merkitse ylemmät luetuksi",
|
||||||
"markBelow": "Merkitse alemmat luetuksi",
|
"markBelow": "Merkitse alemmat luetuksi",
|
||||||
"markRead": "Merkitse luetuksi",
|
"markRead": "Merkitse luetuksi",
|
||||||
"markUnread": "Merkitse lukemattomaksi",
|
"markUnread": "Merkitse lukemattomaksi",
|
||||||
"notify": "Ilmoita, jos haetaan taustalla",
|
"notify": "Ilmoita, jos haetaan taustalla",
|
||||||
"reload": "Ladataanko uudelleen?",
|
"reload": "Ladataanko uudelleen?",
|
||||||
"star": "Tähti",
|
"star": "Tähti",
|
||||||
"unhide": "Näytä artikkeli",
|
"unhide": "Näytä artikkeli",
|
||||||
"unstar": "Poista tähti",
|
"unstar": "Poista tähti",
|
||||||
"untitled": "(Nimetön)"
|
"untitled": "(Nimetön)"
|
||||||
},
|
},
|
||||||
"cancel": "Peruuta",
|
"cancel": "Peruuta",
|
||||||
"close": "Sulje",
|
"close": "Sulje",
|
||||||
"confirm": "Vahvista",
|
"confirm": "Vahvista",
|
||||||
"confirmMarkAll": "Haluatko todella merkitä kaikki tämän sivun artikkelit luetuiksi?",
|
"confirmMarkAll": "Haluatko todella merkitä kaikki tämän sivun artikkelit luetuiksi?",
|
||||||
"context": {
|
"context": {
|
||||||
"cardView": "Kortit",
|
"cardView": "Kortit",
|
||||||
"caseSensitive": "Vain sama merkkikoko",
|
"caseSensitive": "Vain sama merkkikoko",
|
||||||
"compactView": "Kompakti",
|
"compactView": "Kompakti",
|
||||||
"copy": "Kopioi",
|
"copy": "Kopioi",
|
||||||
"copyImage": "Kopioi kuva",
|
"copyImage": "Kopioi kuva",
|
||||||
"copyImageURL": "Kopioi kuvalinkki",
|
"copyImageURL": "Kopioi kuvalinkki",
|
||||||
"copyTitle": "Kopioi otsikko",
|
"copyTitle": "Kopioi otsikko",
|
||||||
"copyURL": "Kopioi linkki",
|
"copyURL": "Kopioi linkki",
|
||||||
"fadeRead": "Häivytä luetut artikkelit",
|
"fadeRead": "Häivytä luetut artikkelit",
|
||||||
"filter": "Suodatus",
|
"filter": "Suodatus",
|
||||||
"fullSearch": "Hae koko tekstistä",
|
"fullSearch": "Hae koko tekstistä",
|
||||||
"listView": "Lista",
|
"listView": "Lista",
|
||||||
"magazineView": "Lehti",
|
"magazineView": "Lehti",
|
||||||
"manageSources": "Hallitse lähteitä",
|
"manageSources": "Hallitse lähteitä",
|
||||||
"read": "Luettu",
|
"read": "Luettu",
|
||||||
"saveImageAs": "Tallenna kuva nimellä…",
|
"saveImageAs": "Tallenna kuva nimellä…",
|
||||||
"search": "Hae \"{text}\" {engine}lla",
|
"search": "Hae \"{text}\" {engine}lla",
|
||||||
"share": "Jaa",
|
"share": "Jaa",
|
||||||
"showCover": "Näytä artikkelin kuva",
|
"showCover": "Näytä artikkelin kuva",
|
||||||
"showHidden": "Näytä piilotetut artikkelit",
|
"showHidden": "Näytä piilotetut artikkelit",
|
||||||
"showSnippet": "Näytä katkelma",
|
"showSnippet": "Näytä katkelma",
|
||||||
"starredOnly": "Vain tähdellä merkityt",
|
"starredOnly": "Vain tähdellä merkityt",
|
||||||
"unreadOnly": "Vain lukemattomat",
|
"unreadOnly": "Vain lukemattomat",
|
||||||
"view": "Näkymä"
|
"view": "Näkymä"
|
||||||
},
|
},
|
||||||
"create": "Luo",
|
"create": "Luo",
|
||||||
"dangerButton": "Vahvistetaanko {action}?",
|
"dangerButton": "Vahvistetaanko {action}?",
|
||||||
"delete": "Poista",
|
"delete": "Poista",
|
||||||
"edit": "Muokkaa",
|
"edit": "Muokkaa",
|
||||||
"emptyField": "Tämä kenttä ei voi olla tyhjä.",
|
"emptyField": "Tämä kenttä ei voi olla tyhjä.",
|
||||||
"emptyName": "Tämä kenttä ei voi olla tyhjä.",
|
"emptyName": "Tämä kenttä ei voi olla tyhjä.",
|
||||||
"followSystem": "Käytä järjestelmän teemaa",
|
"followSystem": "Käytä järjestelmän teemaa",
|
||||||
"groups": {
|
"groups": {
|
||||||
"addToGroup": "Lisää ...",
|
"addToGroup": "Lisää ...",
|
||||||
"capacity": "Määrä",
|
"capacity": "Määrä",
|
||||||
"chooseGroup": "Valitse ryhmä",
|
"chooseGroup": "Valitse ryhmä",
|
||||||
"create": "Luo ryhmä",
|
"create": "Luo ryhmä",
|
||||||
"deleteGroup": "Poista ryhmä",
|
"deleteGroup": "Poista ryhmä",
|
||||||
"deleteSource": "Poista ryhmästä",
|
"deleteSource": "Poista ryhmästä",
|
||||||
"editName": "Muokkaa nimeä",
|
"editName": "Muokkaa nimeä",
|
||||||
"enterName": "Anna nimi",
|
"enterName": "Anna nimi",
|
||||||
"exist": "Tämä ryhmä on jo olemassa.",
|
"exist": "Tämä ryhmä on jo olemassa.",
|
||||||
"exitGroup": "Takaisin ryhmiin",
|
"exitGroup": "Takaisin ryhmiin",
|
||||||
"group": "Ryhmä",
|
"group": "Ryhmä",
|
||||||
"groupHint": "Kaksoisnapsauta ryhmää muokataksesi sen lähteitä. Järjestä uudelleen vetämällä ja pudottamalla.",
|
"groupHint": "Kaksoisnapsauta ryhmää muokataksesi sen lähteitä. Järjestä uudelleen vetämällä ja pudottamalla.",
|
||||||
"selectedGroup": "Valittu ryhmä",
|
"selectedGroup": "Valittu ryhmä",
|
||||||
"selectedSource": "Valittu lähde",
|
"selectedSource": "Valittu lähde",
|
||||||
"source": "Lähde",
|
"source": "Lähde",
|
||||||
"sourceHint": "Järjestä uudelleen vetämällä ja pudottamalla lähteitä.",
|
"sourceHint": "Järjestä uudelleen vetämällä ja pudottamalla lähteitä.",
|
||||||
"type": "Tyyppi"
|
"type": "Tyyppi"
|
||||||
},
|
},
|
||||||
"icon": "Kuvake",
|
"icon": "Kuvake",
|
||||||
"loadMore": "Lataa lisää",
|
"loadMore": "Lataa lisää",
|
||||||
"log": {
|
"log": {
|
||||||
"empty": "Ei ilmoituksia",
|
"empty": "Ei ilmoituksia",
|
||||||
"fetchFailure": "Lähteen \"{name}\" lataaminen epäonnistui.",
|
"fetchFailure": "Lähteen \"{name}\" lataaminen epäonnistui.",
|
||||||
"fetchSuccess": "Noudettiin onnistuneesti {count, plural, =1 {# artikkeli} other {# artikkelia}}.",
|
"fetchSuccess": "Noudettiin onnistuneesti {count, plural, =1 {# artikkeli} other {# artikkelia}}.",
|
||||||
"networkError": "Tapahtui verkkovirhe.",
|
"networkError": "Tapahtui verkkovirhe.",
|
||||||
"parseError": "XML-syötteen jäsentämisessä tapahtui virhe.",
|
"parseError": "XML-syötteen jäsentämisessä tapahtui virhe.",
|
||||||
"syncFailure": "Palvelun kanssa synkronointi epäonnistui"
|
"syncFailure": "Palvelun kanssa synkronointi epäonnistui"
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"close": "Sulje valikko",
|
"close": "Sulje valikko",
|
||||||
"subscriptions": "Tilaukset"
|
"subscriptions": "Tilaukset"
|
||||||
},
|
},
|
||||||
"more": "Lisää",
|
"more": "Lisää",
|
||||||
"name": "Nimi",
|
"name": "Nimi",
|
||||||
"nav": {
|
"nav": {
|
||||||
"markAllRead": "Merkitse kaikki luetuksi",
|
"markAllRead": "Merkitse kaikki luetuksi",
|
||||||
"maximize": "Laajenna",
|
"maximize": "Laajenna",
|
||||||
"menu": "Valikko",
|
"menu": "Valikko",
|
||||||
"minimize": "Pienennä",
|
"minimize": "Pienennä",
|
||||||
"notifications": "Ilmoitukset",
|
"notifications": "Ilmoitukset",
|
||||||
"refresh": "Päivitä",
|
"refresh": "Päivitä",
|
||||||
"settings": "Asetukset",
|
"settings": "Asetukset",
|
||||||
"view": "Näkymä"
|
"view": "Näkymä"
|
||||||
},
|
},
|
||||||
"openExternal": "Avaa selaimessa",
|
"openExternal": "Avaa selaimessa",
|
||||||
"rules": {
|
"rules": {
|
||||||
"action": "Toiminto",
|
"action": "Toiminto",
|
||||||
"badRegex": "Virheellinen sääntö.",
|
"badRegex": "Virheellinen sääntö.",
|
||||||
"content": "Sisältö",
|
"content": "Sisältö",
|
||||||
"creator": "Kirjoittaja",
|
"creator": "Kirjoittaja",
|
||||||
"fullSearch": "Otsikko tai sisältö",
|
"fullSearch": "Otsikko tai sisältö",
|
||||||
"help": "Lisätietoja",
|
"help": "Lisätietoja",
|
||||||
"hint": "Sääntöjä sovelletaan järjestyksessä. Järjestä uudelleen vetämällä ja pudottamalla.",
|
"hint": "Sääntöjä sovelletaan järjestyksessä. Järjestä uudelleen vetämällä ja pudottamalla.",
|
||||||
"if": "Jos",
|
"if": "Jos",
|
||||||
"intro": "Merkitse artikkelit automaattisesti tai lähetä ilmoituksia säännöllisin lausekkein.",
|
"intro": "Merkitse artikkelit automaattisesti tai lähetä ilmoituksia säännöllisin lausekkein.",
|
||||||
"match": "täsmää",
|
"match": "täsmää",
|
||||||
"new": "Uusi sääntö",
|
"new": "Uusi sääntö",
|
||||||
"notMatch": "ei täsmää",
|
"notMatch": "ei täsmää",
|
||||||
"regex": "Säännöllinen lauseke",
|
"regex": "Säännöllinen lauseke",
|
||||||
"selectAction": "Valitse toiminnot",
|
"selectAction": "Valitse toiminnot",
|
||||||
"selectSource": "Valitse lähde",
|
"selectSource": "Valitse lähde",
|
||||||
"source": "Lähde",
|
"source": "Lähde",
|
||||||
"test": "Testaa sääntöä",
|
"test": "Testaa sääntöä",
|
||||||
"then": "Sitten",
|
"then": "Sitten",
|
||||||
"title": "Otsikko"
|
"title": "Otsikko"
|
||||||
},
|
},
|
||||||
"search": "Hae",
|
"search": "Hae",
|
||||||
"searchEngine": {
|
"searchEngine": {
|
||||||
"baidu": "Baidu",
|
"baidu": "Baidu",
|
||||||
"bing": "Bing",
|
"bing": "Bing",
|
||||||
"duckduckgo": "DuckDuckGo",
|
"duckduckgo": "DuckDuckGo",
|
||||||
"google": "Google",
|
"google": "Google",
|
||||||
"name": "Hakukone"
|
"name": "Hakukone"
|
||||||
},
|
},
|
||||||
"service": {
|
"service": {
|
||||||
"endpoint": "Osoite",
|
"endpoint": "Osoite",
|
||||||
"exportToLite": "Vie Fluent Reader Lite -ohjelmaan",
|
"exportToLite": "Vie Fluent Reader Lite -ohjelmaan",
|
||||||
"failure": "Palveluun ei voi muodostaa yhteyttä",
|
"failure": "Palveluun ei voi muodostaa yhteyttä",
|
||||||
"failureHint": "Tarkista palvelun asetukset tai verkkoyhteytesi.",
|
"failureHint": "Tarkista palvelun asetukset tai verkkoyhteytesi.",
|
||||||
"fetchLimit": "Synkronointiraja",
|
"fetchLimit": "Synkronointiraja",
|
||||||
"fetchLimitNum": "{count} viimeisintä artikkelia",
|
"fetchLimitNum": "{count} viimeisintä artikkelia",
|
||||||
"fetchUnlimited": "Rajoittamaton (ei suositella)",
|
"fetchUnlimited": "Rajoittamaton (ei suositella)",
|
||||||
"groupsWarning": "Ryhmiä ei synkronoida automaattisesti palvelun kanssa.",
|
"groupsWarning": "Ryhmiä ei synkronoida automaattisesti palvelun kanssa.",
|
||||||
"importGroups": "Tuo ryhmät",
|
"importGroups": "Tuo ryhmät",
|
||||||
"intro": "Synkronoi laitteiden välillä RSS-palveluiden kanssa.",
|
"intro": "Synkronoi laitteiden välillä RSS-palveluiden kanssa.",
|
||||||
"overwriteWarning": "Paikalliset lähteet poistetaan, jos niitä on palvelussa.",
|
"overwriteWarning": "Paikalliset lähteet poistetaan, jos niitä on palvelussa.",
|
||||||
"password": "Salasana",
|
"password": "Salasana",
|
||||||
"rateLimitWarning": "Rajapintakäytön rajoituksien välttämiseksi sinun on luotava oma API-avain.",
|
"rateLimitWarning": "Rajapintakäytön rajoituksien välttämiseksi sinun on luotava oma API-avain.",
|
||||||
"removeAd": "Poista mainos",
|
"removeAd": "Poista mainos",
|
||||||
"select": "Valitse palvelu",
|
"select": "Valitse palvelu",
|
||||||
"suggest": "Ehdota uutta palvelua",
|
"suggest": "Ehdota uutta palvelua",
|
||||||
"unchanged": "Ei muutoksia",
|
"unchanged": "Ei muutoksia",
|
||||||
"username": "Käyttäjätunnus"
|
"username": "Käyttäjätunnus"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"about": "Tietoja",
|
"about": "Tietoja",
|
||||||
"app": "Asetukset",
|
"app": "Asetukset",
|
||||||
"exit": "Poistu asetuksista",
|
"exit": "Poistu asetuksista",
|
||||||
"feedback": "Palaute",
|
"feedback": "Palaute",
|
||||||
"fetching": "Päivitetään lähteitä, odota…",
|
"fetching": "Päivitetään lähteitä, odota…",
|
||||||
"grouping": "Ryhmät",
|
"grouping": "Ryhmät",
|
||||||
"name": "Asetukset",
|
"name": "Asetukset",
|
||||||
"openSource": "Avoin lähdekoodi",
|
"openSource": "Avoin lähdekoodi",
|
||||||
"rules": "Säännöt",
|
"rules": "Säännöt",
|
||||||
"service": "Palvelu",
|
"service": "Palvelu",
|
||||||
"shortcuts": "Pikakomennot",
|
"shortcuts": "Pikakomennot",
|
||||||
"sources": "Lähteet",
|
"sources": "Lähteet",
|
||||||
"version": "Versio",
|
"version": "Versio",
|
||||||
"writeError": "Tiedostoa kirjoitettaessa tapahtui virhe."
|
"writeError": "Tiedostoa kirjoitettaessa tapahtui virhe."
|
||||||
},
|
},
|
||||||
"sources": {
|
"sources": {
|
||||||
"add": "Lisää lähde",
|
"add": "Lisää lähde",
|
||||||
"badIcon": "Virheellinen kuvake",
|
"badIcon": "Virheellinen kuvake",
|
||||||
"badUrl": "Virheellinen URL",
|
"badUrl": "Virheellinen URL",
|
||||||
"delete": "Poista lähde",
|
"delete": "Poista lähde",
|
||||||
"deleteWarning": "Lähde ja kaikki tallennetut artikkelit poistetaan.",
|
"deleteWarning": "Lähde ja kaikki tallennetut artikkelit poistetaan.",
|
||||||
"editName": "Muokkaa nimeä",
|
"editName": "Muokkaa nimeä",
|
||||||
"errorAdd": "Lähdettä lisättäessä tapahtui virhe.",
|
"errorAdd": "Lähdettä lisättäessä tapahtui virhe.",
|
||||||
"errorImport": "Virhe tuotaessa {count, plural, =1 {# lähdettä} other {# lähteitä}}.",
|
"errorImport": "Virhe tuotaessa {count, plural, =1 {# lähdettä} other {# lähteitä}}.",
|
||||||
"errorParse": "OPML -tiedoston jäsentämisessä tapahtui virhe.",
|
"errorParse": "OPML -tiedoston jäsentämisessä tapahtui virhe.",
|
||||||
"errorParseHint": "Varmista, että tiedosto ei ole vioittunut ja että se on koodattu UTF-8: lla.",
|
"errorParseHint": "Varmista, että tiedosto ei ole vioittunut ja että se on koodattu UTF-8: lla.",
|
||||||
"exist": "Tämä lähde on jo olemassa.",
|
"exist": "Tämä lähde on jo olemassa.",
|
||||||
"export": "Vie",
|
"export": "Vie",
|
||||||
"fetchFrequency": "Tietojen hakemisen raja",
|
"fetchFrequency": "Tietojen hakemisen raja",
|
||||||
"import": "Tuo",
|
"import": "Tuo",
|
||||||
"inputUrl": "Syötä URL",
|
"inputUrl": "Syötä URL",
|
||||||
"loadWebpage": "Lataa verkkosivu",
|
"loadWebpage": "Lataa verkkosivu",
|
||||||
"name": "Lähteen nimi",
|
"name": "Lähteen nimi",
|
||||||
"openTarget": "Avaa artikkelit oletuksena",
|
"openTarget": "Avaa artikkelit oletuksena",
|
||||||
"opmlFile": "OPML -tiedosto",
|
"opmlFile": "OPML -tiedosto",
|
||||||
"rssText": "RSS koko teksti",
|
"rssText": "RSS koko teksti",
|
||||||
"selected": "Valittu lähde",
|
"selected": "Valittu lähde",
|
||||||
"selectedMulti": "Valittu useita lähteitä",
|
"selectedMulti": "Valittu useita lähteitä",
|
||||||
"serviceManaged": "Palvelu hallinnoi tätä lähdettä.",
|
"serviceManaged": "Palvelu hallinnoi tätä lähdettä.",
|
||||||
"serviceWarning": "Täältä tuotuja tai lisättyjä lähteitä ei synkronoida palvelusi kanssa.",
|
"serviceWarning": "Täältä tuotuja tai lisättyjä lähteitä ei synkronoida palvelusi kanssa.",
|
||||||
"unlimited": "Rajoittamaton",
|
"unlimited": "Rajoittamaton",
|
||||||
"untitled": "Lähde"
|
"untitled": "Lähde"
|
||||||
},
|
},
|
||||||
"time": {
|
"time": {
|
||||||
"d": "pv",
|
"d": "pv",
|
||||||
"day": "{d, plural, =1 {# päivä} other {# päivää}}",
|
"day": "{d, plural, =1 {# päivä} other {# päivää}}",
|
||||||
"h": "h",
|
"h": "h",
|
||||||
"hour": "{h, plural, =1 {# tunti} other {# tuntia}}",
|
"hour": "{h, plural, =1 {# tunti} other {# tuntia}}",
|
||||||
"m": "min",
|
"m": "min",
|
||||||
"minute": "{m, plural, =1 {# minutti} other {# minuttia}}",
|
"minute": "{m, plural, =1 {# minutti} other {# minuttia}}",
|
||||||
"now": "nyt"
|
"now": "nyt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -125,7 +125,7 @@
|
|||||||
"errorParse": "Une erreur s'est produite lors de l'analyse du fichier OPML.",
|
"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.",
|
"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}}.",
|
"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",
|
"opmlFile": "Fichier OPML",
|
||||||
"name": "Nom de la source",
|
"name": "Nom de la source",
|
||||||
"editName": "Modifier le nom",
|
"editName": "Modifier le nom",
|
||||||
@ -229,4 +229,4 @@
|
|||||||
"fetchInterval": "Intervalle de récupération automatique",
|
"fetchInterval": "Intervalle de récupération automatique",
|
||||||
"never": "Jamais"
|
"never": "Jamais"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,236 +1,235 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
"allArticles": "Tutti gli articoli",
|
"allArticles": "Tutti gli articoli",
|
||||||
"add": "Aggiungi",
|
"add": "Aggiungi",
|
||||||
"create": "Crea",
|
"create": "Crea",
|
||||||
"icon": "Icona",
|
"icon": "Icona",
|
||||||
"name": "Nome",
|
"name": "Nome",
|
||||||
"openExternal": "Apri Esternamente",
|
"openExternal": "Apri Esternamente",
|
||||||
"emptyName": "Questo campo non puo essere vuoto",
|
"emptyName": "Questo campo non puo essere vuoto",
|
||||||
"emptyField": "Questo campo non puo essere vuoto",
|
"emptyField": "Questo campo non puo essere vuoto",
|
||||||
"edit": "Modifica",
|
"edit": "Modifica",
|
||||||
"delete": "Elimina",
|
"delete": "Elimina",
|
||||||
"followSystem": "segui impostazioni di sistema",
|
"followSystem": "segui impostazioni di sistema",
|
||||||
"more": "di più",
|
"more": "di più",
|
||||||
"close": "Chiudi",
|
"close": "Chiudi",
|
||||||
"search": "Cerca",
|
"search": "Cerca",
|
||||||
"loadMore": "Carica piu feed",
|
"loadMore": "Carica piu feed",
|
||||||
"dangerButton": "Confermi di {action}?",
|
"dangerButton": "Confermi di {action}?",
|
||||||
"confirmMarkAll": "Vuoi veramente segnare tutti i feed di questa pagina come letti?",
|
"confirmMarkAll": "Vuoi veramente segnare tutti i feed di questa pagina come letti?",
|
||||||
"confirm": "Confema",
|
"confirm": "Confema",
|
||||||
"cancel": "Anulla",
|
"cancel": "Anulla",
|
||||||
"time": {
|
"time": {
|
||||||
"now": "ora",
|
"now": "ora",
|
||||||
"m": "m",
|
"m": "m",
|
||||||
"h": "h",
|
"h": "h",
|
||||||
"d": "g",
|
"d": "g",
|
||||||
"minute": "{m, plural, =1 {# minuto} other {# minuti}}",
|
"minute": "{m, plural, =1 {# minuto} other {# minuti}}",
|
||||||
"hour": "{h, plural, =1 {# ora} other {# ore}}",
|
"hour": "{h, plural, =1 {# ora} other {# ore}}",
|
||||||
"day": "{d, plural, =1 {# giorno} other {# giorni}}"
|
"day": "{d, plural, =1 {# giorno} other {# giorni}}"
|
||||||
},
|
},
|
||||||
"log": {
|
"log": {
|
||||||
"empty": "Non ci sono notifiche",
|
"empty": "Non ci sono notifiche",
|
||||||
"fetchFailure": "Errore nel caricare la fonte \"{name}\".",
|
"fetchFailure": "Errore nel caricare la fonte \"{name}\".",
|
||||||
"fetchSuccess": "{count, plural, =1 {# articolo} other {# articoli}} caricato con successo",
|
"fetchSuccess": "{count, plural, =1 {# articolo} other {# articoli}} caricato con successo",
|
||||||
"networkError": "è occorso un errore di rete",
|
"networkError": "è occorso un errore di rete",
|
||||||
"parseError": "è occorso un errore nel anallizzare il feed rss",
|
"parseError": "è occorso un errore nel anallizzare il feed rss",
|
||||||
"syncFailure": "Errore nel sicronizzarsi con il servizio"
|
"syncFailure": "Errore nel sicronizzarsi con il servizio"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"menu": "Menu",
|
"menu": "Menu",
|
||||||
"refresh": "Aggiorna",
|
"refresh": "Aggiorna",
|
||||||
"markAllRead": "Segna tutti come letti",
|
"markAllRead": "Segna tutti come letti",
|
||||||
"notifications": "Notifiche",
|
"notifications": "Notifiche",
|
||||||
"view": "View",
|
"view": "View",
|
||||||
"settings": "Impostazioni",
|
"settings": "Impostazioni",
|
||||||
"minimize": "Riduci a Icona",
|
"minimize": "Riduci a Icona",
|
||||||
"maximize": "Ingrandisci"
|
"maximize": "Ingrandisci"
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"close": "Chiudi menu",
|
"close": "Chiudi menu",
|
||||||
"subscriptions": "Iscrizioni"
|
"subscriptions": "Iscrizioni"
|
||||||
},
|
},
|
||||||
"article": {
|
"article": {
|
||||||
"error": "Errore nel caricare articolo",
|
"error": "Errore nel caricare articolo",
|
||||||
"reload": "Aggiorna?",
|
"reload": "Aggiorna?",
|
||||||
"empty": "Non ci sono articoli",
|
"empty": "Non ci sono articoli",
|
||||||
"untitled": "(Senza titolo)",
|
"untitled": "(Senza titolo)",
|
||||||
"hide": "Nascondi articolo",
|
"hide": "Nascondi articolo",
|
||||||
"unhide": "Mostra articolo",
|
"unhide": "Mostra articolo",
|
||||||
"markRead": "Segna come letto",
|
"markRead": "Segna come letto",
|
||||||
"markUnread": "Segna come non letto",
|
"markUnread": "Segna come non letto",
|
||||||
"markAbove": "Segna precedenti come letti",
|
"markAbove": "Segna precedenti come letti",
|
||||||
"markBelow": "Segna successivi come letti",
|
"markBelow": "Segna successivi come letti",
|
||||||
"star": "salva",
|
"star": "salva",
|
||||||
"unstar": "rimuovi dai salvati",
|
"unstar": "rimuovi dai salvati",
|
||||||
"fontSize": "Dimensione testo",
|
"fontSize": "Dimensione testo",
|
||||||
"loadWebpage": "Carica pagina",
|
"loadWebpage": "Carica pagina",
|
||||||
"loadFull": "Carica tutto il contenuto",
|
"loadFull": "Carica tutto il contenuto",
|
||||||
"notify": "Notifica se caricato in background",
|
"notify": "Notifica se caricato in background",
|
||||||
"dontNotify": "Non notificare"
|
"dontNotify": "Non notificare"
|
||||||
},
|
},
|
||||||
"context": {
|
"context": {
|
||||||
"share": "Convidi",
|
"share": "Convidi",
|
||||||
"read": "Leggi",
|
"read": "Leggi",
|
||||||
"copyTitle": "Copia titolo",
|
"copyTitle": "Copia titolo",
|
||||||
"copyURL": "Copia link",
|
"copyURL": "Copia link",
|
||||||
"copy": "Copia",
|
"copy": "Copia",
|
||||||
"search": "Cerca \"{text}\" on {engine}",
|
"search": "Cerca \"{text}\" on {engine}",
|
||||||
"view": "Visualizza",
|
"view": "Visualizza",
|
||||||
"cardView": "Card view",
|
"cardView": "Card view",
|
||||||
"listView": "List view",
|
"listView": "List view",
|
||||||
"magazineView": "Magazine view",
|
"magazineView": "Magazine view",
|
||||||
"compactView": "Compact view",
|
"compactView": "Compact view",
|
||||||
"filter": "Filtra",
|
"filter": "Filtra",
|
||||||
"unreadOnly": "Solo non letti",
|
"unreadOnly": "Solo non letti",
|
||||||
"starredOnly": "Solo Salvati",
|
"starredOnly": "Solo Salvati",
|
||||||
"fullSearch": "Cerca in tutto il lesto",
|
"fullSearch": "Cerca in tutto il lesto",
|
||||||
"showHidden": "Visualizza articoli nascosti",
|
"showHidden": "Visualizza articoli nascosti",
|
||||||
"manageSources": "Gestisci fonti",
|
"manageSources": "Gestisci fonti",
|
||||||
"saveImageAs": "Salva immagine come …",
|
"saveImageAs": "Salva immagine come …",
|
||||||
"copyImage": "Copia immagine",
|
"copyImage": "Copia immagine",
|
||||||
"copyImageURL": "Copia immagine link",
|
"copyImageURL": "Copia immagine link",
|
||||||
"caseSensitive": "Case sensitive",
|
"caseSensitive": "Case sensitive",
|
||||||
"showCover": "Mostra copertina",
|
"showCover": "Mostra copertina",
|
||||||
"showSnippet": "Show snippet",
|
"showSnippet": "Show snippet",
|
||||||
"fadeRead": "Scolorisci articoli letti"
|
"fadeRead": "Scolorisci articoli letti"
|
||||||
},
|
},
|
||||||
"searchEngine": {
|
"searchEngine": {
|
||||||
"name": "Motore di ricerca",
|
"name": "Motore di ricerca",
|
||||||
"google": "Google",
|
"google": "Google",
|
||||||
"bing": "Bing",
|
"bing": "Bing",
|
||||||
"baidu": "Baidu",
|
"baidu": "Baidu",
|
||||||
"duckduckgo": "DuckDuckGo"
|
"duckduckgo": "DuckDuckGo"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"writeError": "è occorso un errore nello scrivere il file",
|
"writeError": "è occorso un errore nello scrivere il file",
|
||||||
"name": "Impostazioni",
|
"name": "Impostazioni",
|
||||||
"fetching": "Aggiornamento delle fonti..Attendi",
|
"fetching": "Aggiornamento delle fonti..Attendi",
|
||||||
"exit": "Esci dalle Impostazioni",
|
"exit": "Esci dalle Impostazioni",
|
||||||
"sources": "Fonti",
|
"sources": "Fonti",
|
||||||
"grouping": "Gruppi",
|
"grouping": "Gruppi",
|
||||||
"rules": "Regole",
|
"rules": "Regole",
|
||||||
"service": "Servizio",
|
"service": "Servizio",
|
||||||
"app": "Preferenze",
|
"app": "Preferenze",
|
||||||
"about": "Informazioni",
|
"about": "Informazioni",
|
||||||
"version": "Versione",
|
"version": "Versione",
|
||||||
"shortcuts": "Shortcuts",
|
"shortcuts": "Shortcuts",
|
||||||
"openSource": "Open source",
|
"openSource": "Open source",
|
||||||
"feedback": "Feedback"
|
"feedback": "Feedback"
|
||||||
},
|
},
|
||||||
"sources": {
|
"sources": {
|
||||||
"serviceWarning": "le fonti importate o aggiunte qui non saranno sincronizzate con il tuo servizio",
|
"serviceWarning": "le fonti importate o aggiunte qui non saranno sincronizzate con il tuo servizio",
|
||||||
"serviceManaged": "la fonte è gesita dal tuo servizio",
|
"serviceManaged": "la fonte è gesita dal tuo servizio",
|
||||||
"untitled": "Fonte",
|
"untitled": "Fonte",
|
||||||
"errorAdd": "Un errore è occorso nel caricare la fonte",
|
"errorAdd": "Un errore è occorso nel caricare la fonte",
|
||||||
"errorParse": "Un errore è occorso nell analizzare il file OPML",
|
"errorParse": "Un errore è occorso nell analizzare il file OPML",
|
||||||
"errorParseInt":"Assicurati che il file non sia corrotto è sia in formato utf-8",
|
"errorParseInt": "Assicurati che il file non sia corrotto è sia in formato utf-8",
|
||||||
"errorImport": "Errore nel importare {count, plural, =1 {# fonte} other {# fonti}}.",
|
"errorImport": "Errore nel importare {count, plural, =1 {# fonte} other {# fonti}}.",
|
||||||
"exist": "Questa fonte è gia stata aggiunta",
|
"exist": "Questa fonte è gia stata aggiunta",
|
||||||
"opmlFile": "OPML File",
|
"opmlFile": "OPML File",
|
||||||
"name": "Nome fonte",
|
"name": "Nome fonte",
|
||||||
"editName": "Modifica Nome",
|
"editName": "Modifica Nome",
|
||||||
"fetchFrequency": "Limite frecquenza di aggiornamento",
|
"fetchFrequency": "Limite frecquenza di aggiornamento",
|
||||||
"unlimited": "Senza limite",
|
"unlimited": "Senza limite",
|
||||||
"openTarget": "Luogo predefinito di apertura degli articoli",
|
"openTarget": "Luogo predefinito di apertura degli articoli",
|
||||||
"delete": "Elimina fonte",
|
"delete": "Elimina fonte",
|
||||||
"add": "Aggiungi fonte",
|
"add": "Aggiungi fonte",
|
||||||
"import": "Importa",
|
"import": "Importa",
|
||||||
"export": "Esporta",
|
"export": "Esporta",
|
||||||
"rssText": "RSS visualizza tutto il testo",
|
"rssText": "RSS visualizza tutto il testo",
|
||||||
"loadWebpage": "Carica pagina",
|
"loadWebpage": "Carica pagina",
|
||||||
"inputUrl": "Inserisci URL",
|
"inputUrl": "Inserisci URL",
|
||||||
"badIcon": "Icona non valida",
|
"badIcon": "Icona non valida",
|
||||||
"badUrl": "URL non valido",
|
"badUrl": "URL non valido",
|
||||||
"deleteWarning": "La fonte è tutti i relativi articoli salvati verranno eliminati",
|
"deleteWarning": "La fonte è tutti i relativi articoli salvati verranno eliminati",
|
||||||
"selected": "Seleziona fonte",
|
"selected": "Seleziona fonte",
|
||||||
"selectedMulti": "Seleziona più fonti"
|
"selectedMulti": "Seleziona più fonti"
|
||||||
},
|
},
|
||||||
"groups": {
|
"groups": {
|
||||||
"exist": "Questo gruppo già esiste",
|
"exist": "Questo gruppo già esiste",
|
||||||
"type": "Tipo",
|
"type": "Tipo",
|
||||||
"group": "Gruppo",
|
"group": "Gruppo",
|
||||||
"source": "Fonte",
|
"source": "Fonte",
|
||||||
"capacity": "Capacita",
|
"capacity": "Capacita",
|
||||||
"exitGroup": "Ritorna ai gruppi",
|
"exitGroup": "Ritorna ai gruppi",
|
||||||
"deleteSource": "Rimuovi dal gruppo",
|
"deleteSource": "Rimuovi dal gruppo",
|
||||||
"sourceHint": "Clicca e trascina le fonti per ordinarle",
|
"sourceHint": "Clicca e trascina le fonti per ordinarle",
|
||||||
"create": "Crea gruppo",
|
"create": "Crea gruppo",
|
||||||
"selectedGroup": "Seleziona gruppo",
|
"selectedGroup": "Seleziona gruppo",
|
||||||
"selectedSource": "Seleziona fonte",
|
"selectedSource": "Seleziona fonte",
|
||||||
"enterName": "Inserisci nome",
|
"enterName": "Inserisci nome",
|
||||||
"editName": "Modificaname",
|
"editName": "Modificaname",
|
||||||
"deleteGroup": "Elimina Gruppo",
|
"deleteGroup": "Elimina Gruppo",
|
||||||
"chooseGroup": "Seleziona un gruppo",
|
"chooseGroup": "Seleziona un gruppo",
|
||||||
"addToGroup": "Aggiungi a ...",
|
"addToGroup": "Aggiungi a ...",
|
||||||
"groupHint": "Doppio-Click sul gruppo per modificare le fonti. Clicca e trascina le fonti per ordinarle"
|
"groupHint": "Doppio-Click sul gruppo per modificare le fonti. Clicca e trascina le fonti per ordinarle"
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"intro": "automaticamentente seleziona articoli o manda notifiche tramite Regex",
|
"intro": "automaticamentente seleziona articoli o manda notifiche tramite Regex",
|
||||||
"help": "Per saperne di più",
|
"help": "Per saperne di più",
|
||||||
"source": "Fonte",
|
"source": "Fonte",
|
||||||
"selectSource": "Seleziona una fonte",
|
"selectSource": "Seleziona una fonte",
|
||||||
"new": "Nuova Regola",
|
"new": "Nuova Regola",
|
||||||
"if": "Se",
|
"if": "Se",
|
||||||
"then": "Allora",
|
"then": "Allora",
|
||||||
"title": "Titolo",
|
"title": "Titolo",
|
||||||
"content": "Contenuto",
|
"content": "Contenuto",
|
||||||
"fullSearch": "Titolo o Contenuto",
|
"fullSearch": "Titolo o Contenuto",
|
||||||
"creator": "Autore",
|
"creator": "Autore",
|
||||||
"match": "Risultati",
|
"match": "Risultati",
|
||||||
"notMatch": "Non ci sono risultati",
|
"notMatch": "Non ci sono risultati",
|
||||||
"regex": "Espressione Regolare",
|
"regex": "Espressione Regolare",
|
||||||
"badRegex": "Espressione Regolare Invalida",
|
"badRegex": "Espressione Regolare Invalida",
|
||||||
"action": "Azioni",
|
"action": "Azioni",
|
||||||
"selectAction": "Seleziona azioni",
|
"selectAction": "Seleziona azioni",
|
||||||
"hint": "Le regole verranno applicate sequenzialmente, Clicca e trascina per riordinare",
|
"hint": "Le regole verranno applicate sequenzialmente, Clicca e trascina per riordinare",
|
||||||
"test": "Prova le regole"
|
"test": "Prova le regole"
|
||||||
},
|
},
|
||||||
"service": {
|
"service": {
|
||||||
"intro": "Sincronizza attraverso i dispositivi i servizi RSS",
|
"intro": "Sincronizza attraverso i dispositivi i servizi RSS",
|
||||||
"select": "Seleziona un servizio",
|
"select": "Seleziona un servizio",
|
||||||
"suggest": "Suggerisci un nuovo servizio",
|
"suggest": "Suggerisci un nuovo servizio",
|
||||||
"overwriteWarning": "Le fonti locali verranno eliminate se sono gia presenti nel servizio ",
|
"overwriteWarning": "Le fonti locali verranno eliminate se sono gia presenti nel servizio ",
|
||||||
"groupsWarning": "I gruppi non sono automaticamente sincronizzati con il 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",
|
"rateLimitWarning": "Per evitare un limite nella frecquenza di aggiornamento dei avere una API key personalizzata",
|
||||||
"removeAd": "Rimuovi Annuncio",
|
"removeAd": "Rimuovi Annuncio",
|
||||||
"endpoint": "Indirizzo",
|
"endpoint": "Indirizzo",
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"unchanged": "Non modificato",
|
"unchanged": "Non modificato",
|
||||||
"fetchLimit": "Limite di Sincronizzazione",
|
"fetchLimit": "Limite di Sincronizzazione",
|
||||||
"fetchLimitNum": "{count} articoli recenti",
|
"fetchLimitNum": "{count} articoli recenti",
|
||||||
"importGroups": "Importa gruppi",
|
"importGroups": "Importa gruppi",
|
||||||
"failure": "Impossibile connettersi al servizio",
|
"failure": "Impossibile connettersi al servizio",
|
||||||
"failureHint": "Controlla le impostazioni del servizio o se ci sono problemi di connessione",
|
"failureHint": "Controlla le impostazioni del servizio o se ci sono problemi di connessione",
|
||||||
"fetchUnlimited": "Illimitato (non Raccomandaot)",
|
"fetchUnlimited": "Illimitato (non Raccomandaot)",
|
||||||
"exportToLite": "Esporta a Fluent Reader Lite"
|
"exportToLite": "Esporta a Fluent Reader Lite"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"cleanup": "Pulisci",
|
"cleanup": "Pulisci",
|
||||||
"cache": "Elimina cache",
|
"cache": "Elimina cache",
|
||||||
"cacheSize": "Dimensione Cache {size}",
|
"cacheSize": "Dimensione Cache {size}",
|
||||||
"deleteChoices": "Rimuovi articoli di ... giorni fà",
|
"deleteChoices": "Rimuovi articoli di ... giorni fà",
|
||||||
"confirmDelete": "Rimuovi",
|
"confirmDelete": "Rimuovi",
|
||||||
"daysAgo": "{days, plural, =1 {# giorno} other {# giorni}} fà",
|
"daysAgo": "{days, plural, =1 {# giorno} other {# giorni}} fà",
|
||||||
"deleteAll": "Rimuovi tutti gli articoli",
|
"deleteAll": "Rimuovi tutti gli articoli",
|
||||||
"calculatingSize": "Calcolo dimensione...",
|
"calculatingSize": "Calcolo dimensione...",
|
||||||
"itemSize": "Gli articoli occupano {size} ",
|
"itemSize": "Gli articoli occupano {size} ",
|
||||||
"confirmImport": "Vuoi veramente importare i dati dal file di backup? I dati correnti verranno eliminati",
|
"confirmImport": "Vuoi veramente importare i dati dal file di backup? I dati correnti verranno eliminati",
|
||||||
"data": "Memoria Applicazione",
|
"data": "Memoria Applicazione",
|
||||||
"backup": "Backup",
|
"backup": "Backup",
|
||||||
"restore": "Ripristina",
|
"restore": "Ripristina",
|
||||||
"frData": "Fluent Reader Data",
|
"frData": "Fluent Reader Data",
|
||||||
"language": "Lingua Display",
|
"language": "Lingua Display",
|
||||||
"theme": "Tema",
|
"theme": "Tema",
|
||||||
"lightTheme": "Tema chiaro",
|
"lightTheme": "Tema chiaro",
|
||||||
"darkTheme": "Tema scuro",
|
"darkTheme": "Tema scuro",
|
||||||
"enableProxy": "Abilita Proxy",
|
"enableProxy": "Abilita Proxy",
|
||||||
"badUrl": "URL Invalido",
|
"badUrl": "URL Invalido",
|
||||||
"pac": "Indirizzo PAC",
|
"pac": "Indirizzo PAC",
|
||||||
"setPac": " Imposta 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",
|
"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",
|
"fetchInterval": "Ricarica intervallo Automaticamente",
|
||||||
"never": "Mai"
|
"never": "Mai"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -232,4 +232,4 @@
|
|||||||
"fetchInterval": "フェッチ間隔",
|
"fetchInterval": "フェッチ間隔",
|
||||||
"never": "しない"
|
"never": "しない"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -232,4 +232,4 @@
|
|||||||
"fetchInterval": "Automatisch ophalen",
|
"fetchInterval": "Automatisch ophalen",
|
||||||
"never": "Nooit"
|
"never": "Nooit"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -232,4 +232,4 @@
|
|||||||
"fetchInterval": "Intervalo de atualização automática",
|
"fetchInterval": "Intervalo de atualização automática",
|
||||||
"never": "Nunca"
|
"never": "Nunca"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -229,4 +229,4 @@
|
|||||||
"fetchInterval": "Otomatik getirme aralığı",
|
"fetchInterval": "Otomatik getirme aralığı",
|
||||||
"never": "Asla"
|
"never": "Asla"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -230,4 +230,4 @@
|
|||||||
"fetchInterval": "自动抓取频率",
|
"fetchInterval": "自动抓取频率",
|
||||||
"never": "从不"
|
"never": "从不"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,233 +1,233 @@
|
|||||||
{
|
{
|
||||||
"allArticles": "全部文章",
|
"allArticles": "全部文章",
|
||||||
"add": "新增",
|
"add": "新增",
|
||||||
"create": "新建",
|
"create": "新建",
|
||||||
"icon": "圖示",
|
"icon": "圖示",
|
||||||
"name": "名稱",
|
"name": "名稱",
|
||||||
"openExternal": "在瀏覽器中開啟",
|
"openExternal": "在瀏覽器中開啟",
|
||||||
"emptyName": "名稱不得為空",
|
"emptyName": "名稱不得為空",
|
||||||
"emptyField": "此項不得為空",
|
"emptyField": "此項不得為空",
|
||||||
"edit": "編輯",
|
"edit": "編輯",
|
||||||
"delete": "刪除",
|
"delete": "刪除",
|
||||||
"followSystem": "跟隨系統",
|
"followSystem": "跟隨系統",
|
||||||
"more": "更多",
|
"more": "更多",
|
||||||
"close": "關閉",
|
"close": "關閉",
|
||||||
"search": "搜尋",
|
"search": "搜尋",
|
||||||
"loadMore": "載入更多",
|
"loadMore": "載入更多",
|
||||||
"dangerButton": "確認{action}?",
|
"dangerButton": "確認{action}?",
|
||||||
"confirmMarkAll": "確認將本頁所有文章標為已讀?",
|
"confirmMarkAll": "確認將本頁所有文章標為已讀?",
|
||||||
"confirm": "確認",
|
"confirm": "確認",
|
||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"time": {
|
"time": {
|
||||||
"now": "now",
|
"now": "now",
|
||||||
"m": "m",
|
"m": "m",
|
||||||
"h": "h",
|
"h": "h",
|
||||||
"d": "d",
|
"d": "d",
|
||||||
"minute": "{m}分鐘",
|
"minute": "{m}分鐘",
|
||||||
"hour": "{h}小時",
|
"hour": "{h}小時",
|
||||||
"day": "{d}天"
|
"day": "{d}天"
|
||||||
},
|
},
|
||||||
"log": {
|
"log": {
|
||||||
"empty": "無訊息",
|
"empty": "無訊息",
|
||||||
"fetchFailure": "無法載入訂閱源“{name}”",
|
"fetchFailure": "無法載入訂閱源“{name}”",
|
||||||
"fetchSuccess": "成功載入 {count} 篇文章",
|
"fetchSuccess": "成功載入 {count} 篇文章",
|
||||||
"networkError": "連線訂閱源時出錯",
|
"networkError": "連線訂閱源時出錯",
|
||||||
"parseError": "解析XML資訊流時出錯",
|
"parseError": "解析XML資訊流時出錯",
|
||||||
"syncFailure": "無法與服務同步"
|
"syncFailure": "無法與服務同步"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"menu": "選單",
|
"menu": "選單",
|
||||||
"refresh": "重新整理",
|
"refresh": "重新整理",
|
||||||
"markAllRead": "全部標為已讀",
|
"markAllRead": "全部標為已讀",
|
||||||
"notifications": "訊息",
|
"notifications": "訊息",
|
||||||
"view": "檢視",
|
"view": "檢視",
|
||||||
"settings": "選項",
|
"settings": "選項",
|
||||||
"minimize": "最小化",
|
"minimize": "最小化",
|
||||||
"maximize": "最大化"
|
"maximize": "最大化"
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"close": "關閉選單",
|
"close": "關閉選單",
|
||||||
"subscriptions": "訂閱源"
|
"subscriptions": "訂閱源"
|
||||||
},
|
},
|
||||||
"article": {
|
"article": {
|
||||||
"error": "文章載入失敗",
|
"error": "文章載入失敗",
|
||||||
"reload": "重新載入",
|
"reload": "重新載入",
|
||||||
"empty": "無文章",
|
"empty": "無文章",
|
||||||
"untitled": "(無標題)",
|
"untitled": "(無標題)",
|
||||||
"hide": "隱藏文章",
|
"hide": "隱藏文章",
|
||||||
"unhide": "取消隱藏",
|
"unhide": "取消隱藏",
|
||||||
"markRead": "標為已讀",
|
"markRead": "標為已讀",
|
||||||
"markUnread": "標為未讀",
|
"markUnread": "標為未讀",
|
||||||
"markAbove": "將以上標為已讀",
|
"markAbove": "將以上標為已讀",
|
||||||
"markBelow": "將以下標為已讀",
|
"markBelow": "將以下標為已讀",
|
||||||
"star": "標為星標",
|
"star": "標為星標",
|
||||||
"unstar": "取消星標",
|
"unstar": "取消星標",
|
||||||
"fontSize": "字型大小",
|
"fontSize": "字型大小",
|
||||||
"loadWebpage": "載入網頁",
|
"loadWebpage": "載入網頁",
|
||||||
"loadFull": "抓取全文",
|
"loadFull": "抓取全文",
|
||||||
"notify": "後臺抓取時傳送通知",
|
"notify": "後臺抓取時傳送通知",
|
||||||
"dontNotify": "不傳送通知"
|
"dontNotify": "不傳送通知"
|
||||||
},
|
},
|
||||||
"context": {
|
"context": {
|
||||||
"share": "分享",
|
"share": "分享",
|
||||||
"read": "閱讀",
|
"read": "閱讀",
|
||||||
"copyTitle": "複製標題",
|
"copyTitle": "複製標題",
|
||||||
"copyURL": "複製連結",
|
"copyURL": "複製連結",
|
||||||
"copy": "複製",
|
"copy": "複製",
|
||||||
"search": "使用 {engine} 搜尋“{text}”",
|
"search": "使用 {engine} 搜尋“{text}”",
|
||||||
"view": "檢視",
|
"view": "檢視",
|
||||||
"cardView": "卡片檢視",
|
"cardView": "卡片檢視",
|
||||||
"listView": "列表檢視",
|
"listView": "列表檢視",
|
||||||
"magazineView": "雜誌檢視",
|
"magazineView": "雜誌檢視",
|
||||||
"compactView": "緊湊檢視",
|
"compactView": "緊湊檢視",
|
||||||
"filter": "篩選",
|
"filter": "篩選",
|
||||||
"unreadOnly": "僅未讀文章",
|
"unreadOnly": "僅未讀文章",
|
||||||
"starredOnly": "僅星標文章",
|
"starredOnly": "僅星標文章",
|
||||||
"fullSearch": "在正文中搜尋",
|
"fullSearch": "在正文中搜尋",
|
||||||
"showHidden": "顯示隱藏文章",
|
"showHidden": "顯示隱藏文章",
|
||||||
"manageSources": "管理訂閱源",
|
"manageSources": "管理訂閱源",
|
||||||
"saveImageAs": "將影象另存為",
|
"saveImageAs": "將影象另存為",
|
||||||
"copyImage": "複製影象",
|
"copyImage": "複製影象",
|
||||||
"copyImageURL": "複製影象連結",
|
"copyImageURL": "複製影象連結",
|
||||||
"caseSensitive": "區分大小寫",
|
"caseSensitive": "區分大小寫",
|
||||||
"showCover": "顯示封面",
|
"showCover": "顯示封面",
|
||||||
"showSnippet": "顯示摘要",
|
"showSnippet": "顯示摘要",
|
||||||
"fadeRead": "淡化已讀文章"
|
"fadeRead": "淡化已讀文章"
|
||||||
},
|
},
|
||||||
"searchEngine": {
|
"searchEngine": {
|
||||||
"name": "搜尋引擎",
|
"name": "搜尋引擎",
|
||||||
"bing": "必應",
|
"bing": "必應",
|
||||||
"baidu": "百度"
|
"baidu": "百度"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"writeError": "寫入檔案時發生錯誤",
|
"writeError": "寫入檔案時發生錯誤",
|
||||||
"name": "選項",
|
"name": "選項",
|
||||||
"fetching": "正在更新訂閱源,請稍候…",
|
"fetching": "正在更新訂閱源,請稍候…",
|
||||||
"exit": "退出選項",
|
"exit": "退出選項",
|
||||||
"sources": "訂閱源",
|
"sources": "訂閱源",
|
||||||
"grouping": "分組與排序",
|
"grouping": "分組與排序",
|
||||||
"rules": "規則",
|
"rules": "規則",
|
||||||
"service": "服務",
|
"service": "服務",
|
||||||
"app": "應用偏好",
|
"app": "應用偏好",
|
||||||
"about": "關於",
|
"about": "關於",
|
||||||
"version": "版本",
|
"version": "版本",
|
||||||
"shortcuts": "快捷鍵",
|
"shortcuts": "快捷鍵",
|
||||||
"openSource": "開源項目",
|
"openSource": "開源項目",
|
||||||
"feedback": "反饋"
|
"feedback": "反饋"
|
||||||
},
|
},
|
||||||
"sources": {
|
"sources": {
|
||||||
"serviceWarning": "此處匯入或新增的訂閱源將不會與服務端同步",
|
"serviceWarning": "此處匯入或新增的訂閱源將不會與服務端同步",
|
||||||
"serviceManaged": "該訂閱源由服務端管理",
|
"serviceManaged": "該訂閱源由服務端管理",
|
||||||
"untitled": "訂閱源",
|
"untitled": "訂閱源",
|
||||||
"errorAdd": "新增訂閱源時出錯",
|
"errorAdd": "新增訂閱源時出錯",
|
||||||
"errorParse": "解析OPML檔案時出錯",
|
"errorParse": "解析OPML檔案時出錯",
|
||||||
"errorParseHint": "請確保OPML檔案完整且使用UTF-8編碼。",
|
"errorParseHint": "請確保OPML檔案完整且使用UTF-8編碼。",
|
||||||
"errorImport": "匯入{count}項訂閱源時出錯",
|
"errorImport": "匯入{count}項訂閱源時出錯",
|
||||||
"exist": "該訂閱源已存在",
|
"exist": "該訂閱源已存在",
|
||||||
"opmlFile": "OPML檔案",
|
"opmlFile": "OPML檔案",
|
||||||
"name": "訂閱源名稱",
|
"name": "訂閱源名稱",
|
||||||
"editName": "修改名稱",
|
"editName": "修改名稱",
|
||||||
"fetchFrequency": "抓取頻率限制",
|
"fetchFrequency": "抓取頻率限制",
|
||||||
"unlimited": "無限制",
|
"unlimited": "無限制",
|
||||||
"openTarget": "訂閱源文章開啟方式",
|
"openTarget": "訂閱源文章開啟方式",
|
||||||
"delete": "刪除訂閱源",
|
"delete": "刪除訂閱源",
|
||||||
"add": "新增訂閱源",
|
"add": "新增訂閱源",
|
||||||
"import": "匯入檔案",
|
"import": "匯入檔案",
|
||||||
"export": "匯出檔案",
|
"export": "匯出檔案",
|
||||||
"rssText": "RSS正文",
|
"rssText": "RSS正文",
|
||||||
"loadWebpage": "載入網頁",
|
"loadWebpage": "載入網頁",
|
||||||
"inputUrl": "輸入URL",
|
"inputUrl": "輸入URL",
|
||||||
"badIcon": "圖示不存在或非圖片",
|
"badIcon": "圖示不存在或非圖片",
|
||||||
"badUrl": "請正確輸入URL",
|
"badUrl": "請正確輸入URL",
|
||||||
"deleteWarning": "這將移除訂閱源與所有已儲存的文章",
|
"deleteWarning": "這將移除訂閱源與所有已儲存的文章",
|
||||||
"selected": "選中訂閱源",
|
"selected": "選中訂閱源",
|
||||||
"selectedMulti": "選中多個訂閱源"
|
"selectedMulti": "選中多個訂閱源"
|
||||||
},
|
},
|
||||||
"groups": {
|
"groups": {
|
||||||
"exist": "該分組已存在",
|
"exist": "該分組已存在",
|
||||||
"type": "類型",
|
"type": "類型",
|
||||||
"group": "分組",
|
"group": "分組",
|
||||||
"source": "訂閱源",
|
"source": "訂閱源",
|
||||||
"capacity": "容量",
|
"capacity": "容量",
|
||||||
"exitGroup": "退出分組",
|
"exitGroup": "退出分組",
|
||||||
"deleteSource": "從分組刪除訂閱源",
|
"deleteSource": "從分組刪除訂閱源",
|
||||||
"sourceHint": "拖拽訂閱源以排序",
|
"sourceHint": "拖拽訂閱源以排序",
|
||||||
"create": "新建分組",
|
"create": "新建分組",
|
||||||
"selectedGroup": "選中分組",
|
"selectedGroup": "選中分組",
|
||||||
"selectedSource": "選中訂閱源",
|
"selectedSource": "選中訂閱源",
|
||||||
"enterName": "輸入名稱",
|
"enterName": "輸入名稱",
|
||||||
"editName": "修改名稱",
|
"editName": "修改名稱",
|
||||||
"deleteGroup": "刪除分組",
|
"deleteGroup": "刪除分組",
|
||||||
"chooseGroup": "選擇分組",
|
"chooseGroup": "選擇分組",
|
||||||
"addToGroup": "新增至分組",
|
"addToGroup": "新增至分組",
|
||||||
"groupHint": "雙擊分組以修改訂閱源,可通過拖拽排序"
|
"groupHint": "雙擊分組以修改訂閱源,可通過拖拽排序"
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"intro": "通過正規表示式自動標記文章或推送通知",
|
"intro": "通過正規表示式自動標記文章或推送通知",
|
||||||
"help": "瞭解更多",
|
"help": "瞭解更多",
|
||||||
"source": "訂閱源",
|
"source": "訂閱源",
|
||||||
"selectSource": "選擇一個訂閱源",
|
"selectSource": "選擇一個訂閱源",
|
||||||
"new": "新建規則",
|
"new": "新建規則",
|
||||||
"if": "若",
|
"if": "若",
|
||||||
"then": "則",
|
"then": "則",
|
||||||
"title": "標題",
|
"title": "標題",
|
||||||
"content": "正文",
|
"content": "正文",
|
||||||
"fullSearch": "標題或正文",
|
"fullSearch": "標題或正文",
|
||||||
"creator": "作者",
|
"creator": "作者",
|
||||||
"match": "匹配",
|
"match": "匹配",
|
||||||
"notMatch": "不匹配",
|
"notMatch": "不匹配",
|
||||||
"regex": "正規表示式",
|
"regex": "正規表示式",
|
||||||
"badRegex": "正規表示式非法",
|
"badRegex": "正規表示式非法",
|
||||||
"action": "行為",
|
"action": "行為",
|
||||||
"selectAction": "選擇行為",
|
"selectAction": "選擇行為",
|
||||||
"hint": "規則將按順序執行,拖拽以排序",
|
"hint": "規則將按順序執行,拖拽以排序",
|
||||||
"test": "測試規則"
|
"test": "測試規則"
|
||||||
},
|
},
|
||||||
"service": {
|
"service": {
|
||||||
"intro": "通過 RSS 服務跨裝置保持同步",
|
"intro": "通過 RSS 服務跨裝置保持同步",
|
||||||
"select": "選擇服務",
|
"select": "選擇服務",
|
||||||
"suggest": "建議一項新服務",
|
"suggest": "建議一項新服務",
|
||||||
"overwriteWarning": "若本地與服務端存在URL相同的訂閱源,則本地訂閱源將被刪除",
|
"overwriteWarning": "若本地與服務端存在URL相同的訂閱源,則本地訂閱源將被刪除",
|
||||||
"groupsWarning": "分組不會自動與服務端保持同步",
|
"groupsWarning": "分組不會自動與服務端保持同步",
|
||||||
"rateLimitWarning": "為避免限流,您需要新建自己的 API Key",
|
"rateLimitWarning": "為避免限流,您需要新建自己的 API Key",
|
||||||
"removeAd": "移除廣告",
|
"removeAd": "移除廣告",
|
||||||
"endpoint": "端點",
|
"endpoint": "端點",
|
||||||
"username": "使用者名稱",
|
"username": "使用者名稱",
|
||||||
"password": "密碼",
|
"password": "密碼",
|
||||||
"unchanged": "未更改",
|
"unchanged": "未更改",
|
||||||
"fetchLimit": "同步數量",
|
"fetchLimit": "同步數量",
|
||||||
"fetchLimitNum": "最近 {count} 篇文章",
|
"fetchLimitNum": "最近 {count} 篇文章",
|
||||||
"importGroups": "匯入分組",
|
"importGroups": "匯入分組",
|
||||||
"failure": "連線到服務時出錯",
|
"failure": "連線到服務時出錯",
|
||||||
"failureHint": "請檢查服務配置或網路連線",
|
"failureHint": "請檢查服務配置或網路連線",
|
||||||
"fetchUnlimited": "無限制(不建議)",
|
"fetchUnlimited": "無限制(不建議)",
|
||||||
"exportToLite": "匯出至 Fluent Reader Lite"
|
"exportToLite": "匯出至 Fluent Reader Lite"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"cleanup": "清理",
|
"cleanup": "清理",
|
||||||
"cache": "清空快取",
|
"cache": "清空快取",
|
||||||
"cacheSize": "已快取{size}資料",
|
"cacheSize": "已快取{size}資料",
|
||||||
"deleteChoices": "刪除 … 天前的文章",
|
"deleteChoices": "刪除 … 天前的文章",
|
||||||
"confirmDelete": "刪除文章",
|
"confirmDelete": "刪除文章",
|
||||||
"daysAgo": "{days} 天前",
|
"daysAgo": "{days} 天前",
|
||||||
"deleteAll": "刪除全部文章",
|
"deleteAll": "刪除全部文章",
|
||||||
"calculatingSize": "正在計算佔用空間…",
|
"calculatingSize": "正在計算佔用空間…",
|
||||||
"itemSize": "本地文章約佔用{size}空間",
|
"itemSize": "本地文章約佔用{size}空間",
|
||||||
"confirmImport": "確認要從備份檔案匯入資料嗎?這將清除所有應用資料。",
|
"confirmImport": "確認要從備份檔案匯入資料嗎?這將清除所有應用資料。",
|
||||||
"data": "應用資料",
|
"data": "應用資料",
|
||||||
"backup": "備份",
|
"backup": "備份",
|
||||||
"restore": "還原",
|
"restore": "還原",
|
||||||
"frData": "Fluent Reader資料",
|
"frData": "Fluent Reader資料",
|
||||||
"language": "介面語言",
|
"language": "介面語言",
|
||||||
"theme": "應用主題",
|
"theme": "應用主題",
|
||||||
"lightTheme": "淺色模式",
|
"lightTheme": "淺色模式",
|
||||||
"darkTheme": "深色模式",
|
"darkTheme": "深色模式",
|
||||||
"enableProxy": "啟用代理",
|
"enableProxy": "啟用代理",
|
||||||
"badUrl": "請正確輸入URL",
|
"badUrl": "請正確輸入URL",
|
||||||
"pac": "PAC地址",
|
"pac": "PAC地址",
|
||||||
"setPac": "設定PAC",
|
"setPac": "設定PAC",
|
||||||
"pacHint": "對於Socks代理建議PAC返回“SOCKS5”以啟用代理端解析。關閉代理需重啟應用後生效。",
|
"pacHint": "對於Socks代理建議PAC返回“SOCKS5”以啟用代理端解析。關閉代理需重啟應用後生效。",
|
||||||
"fetchInterval": "自動抓取頻率",
|
"fetchInterval": "自動抓取頻率",
|
||||||
"never": "從不"
|
"never": "從不"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,56 @@
|
|||||||
import intl from "react-intl-universal"
|
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 { 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 { 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 {
|
||||||
import { PageActionTypes, SELECT_PAGE, PageType, selectAllArticles, showItemFromId } from "./page"
|
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 { getCurrentLocale } from "../settings"
|
||||||
import locales from "../i18n/_locales"
|
import locales from "../i18n/_locales"
|
||||||
import { SYNC_SERVICE, ServiceActionTypes } from "./service"
|
import { SYNC_SERVICE, ServiceActionTypes } from "./service"
|
||||||
|
|
||||||
export const enum ContextMenuType {
|
export const enum ContextMenuType {
|
||||||
Hidden, Item, Text, View, Group, Image, MarkRead
|
Hidden,
|
||||||
|
Item,
|
||||||
|
Text,
|
||||||
|
View,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
MarkRead,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum AppLogType {
|
export const enum AppLogType {
|
||||||
Info, Warning, Failure, Article
|
Info,
|
||||||
|
Warning,
|
||||||
|
Failure,
|
||||||
|
Article,
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AppLog {
|
export class AppLog {
|
||||||
@ -24,7 +60,12 @@ export class AppLog {
|
|||||||
iid?: number
|
iid?: number
|
||||||
time: Date
|
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.type = type
|
||||||
this.title = title
|
this.title = title
|
||||||
this.details = details
|
this.details = details
|
||||||
@ -49,24 +90,24 @@ export class AppState {
|
|||||||
display: false,
|
display: false,
|
||||||
changed: false,
|
changed: false,
|
||||||
sids: new Array<number>(),
|
sids: new Array<number>(),
|
||||||
saving: false
|
saving: false,
|
||||||
}
|
}
|
||||||
logMenu = {
|
logMenu = {
|
||||||
display: false,
|
display: false,
|
||||||
notify: false,
|
notify: false,
|
||||||
logs: new Array<AppLog>()
|
logs: new Array<AppLog>(),
|
||||||
}
|
}
|
||||||
|
|
||||||
contextMenu: {
|
contextMenu: {
|
||||||
type: ContextMenuType,
|
type: ContextMenuType
|
||||||
event?: MouseEvent | string,
|
event?: MouseEvent | string
|
||||||
position?: [number, number],
|
position?: [number, number]
|
||||||
target?: [RSSItem, string] | number[] | [string, string]
|
target?: [RSSItem, string] | number[] | [string, string]
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.contextMenu = {
|
this.contextMenu = {
|
||||||
type: ContextMenuType.Hidden
|
type: ContextMenuType.Hidden,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -115,14 +156,21 @@ interface OpenImageMenuAction {
|
|||||||
position: [number, number]
|
position: [number, number]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ContextMenuActionTypes = CloseContextMenuAction | OpenItemMenuAction
|
export type ContextMenuActionTypes =
|
||||||
| OpenTextMenuAction | OpenViewMenuAction | OpenGroupMenuAction | OpenImageMenuAction
|
| CloseContextMenuAction
|
||||||
|
| OpenItemMenuAction
|
||||||
|
| OpenTextMenuAction
|
||||||
|
| OpenViewMenuAction
|
||||||
|
| OpenGroupMenuAction
|
||||||
|
| OpenImageMenuAction
|
||||||
| OpenMarkAllMenuAction
|
| OpenMarkAllMenuAction
|
||||||
|
|
||||||
export const TOGGLE_LOGS = "TOGGLE_LOGS"
|
export const TOGGLE_LOGS = "TOGGLE_LOGS"
|
||||||
export const PUSH_NOTIFICATION = "PUSH_NOTIFICATION"
|
export const PUSH_NOTIFICATION = "PUSH_NOTIFICATION"
|
||||||
|
|
||||||
interface ToggleLogMenuAction { type: typeof TOGGLE_LOGS }
|
interface ToggleLogMenuAction {
|
||||||
|
type: typeof TOGGLE_LOGS
|
||||||
|
}
|
||||||
|
|
||||||
interface PushNotificationAction {
|
interface PushNotificationAction {
|
||||||
type: typeof PUSH_NOTIFICATION
|
type: typeof PUSH_NOTIFICATION
|
||||||
@ -155,7 +203,10 @@ interface FreeMemoryAction {
|
|||||||
type: typeof FREE_MEMORY
|
type: typeof FREE_MEMORY
|
||||||
iids: Set<number>
|
iids: Set<number>
|
||||||
}
|
}
|
||||||
export type SettingsActionTypes = ToggleSettingsAction | SaveSettingsAction | FreeMemoryAction
|
export type SettingsActionTypes =
|
||||||
|
| ToggleSettingsAction
|
||||||
|
| SaveSettingsAction
|
||||||
|
| FreeMemoryAction
|
||||||
|
|
||||||
export function closeContextMenu(): AppThunk {
|
export function closeContextMenu(): AppThunk {
|
||||||
return (dispatch, getState) => {
|
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 {
|
return {
|
||||||
type: OPEN_ITEM_MENU,
|
type: OPEN_ITEM_MENU,
|
||||||
event: event.nativeEvent,
|
event: event.nativeEvent,
|
||||||
item: item,
|
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 {
|
return {
|
||||||
type: OPEN_TEXT_MENU,
|
type: OPEN_TEXT_MENU,
|
||||||
position: position,
|
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 {
|
return {
|
||||||
type: OPEN_GROUP_MENU,
|
type: OPEN_GROUP_MENU,
|
||||||
event: event.nativeEvent,
|
event: event.nativeEvent,
|
||||||
sids: sids
|
sids: sids,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openImageMenu(position: [number, number]): ContextMenuActionTypes {
|
export function openImageMenu(
|
||||||
|
position: [number, number]
|
||||||
|
): ContextMenuActionTypes {
|
||||||
return {
|
return {
|
||||||
type: OPEN_IMAGE_MENU,
|
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 {
|
export function toggleMenu(): AppThunk {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
@ -241,7 +309,7 @@ function freeMemory(): AppThunk {
|
|||||||
}
|
}
|
||||||
dispatch({
|
dispatch({
|
||||||
type: FREE_MEMORY,
|
type: FREE_MEMORY,
|
||||||
iids: iids
|
iids: iids,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -276,7 +344,10 @@ export function pushNotification(item: RSSItem): AppThunk {
|
|||||||
const notification = new Notification(item.title, options)
|
const notification = new Notification(item.title, options)
|
||||||
notification.onclick = () => {
|
notification.onclick = () => {
|
||||||
const state = getState()
|
const state = getState()
|
||||||
if (state.sources[item.source].openTarget === SourceOpenTarget.External) {
|
if (
|
||||||
|
state.sources[item.source].openTarget ===
|
||||||
|
SourceOpenTarget.External
|
||||||
|
) {
|
||||||
window.utils.openExternal(item.link)
|
window.utils.openExternal(item.link)
|
||||||
} else if (!state.app.settings.display) {
|
} else if (!state.app.settings.display) {
|
||||||
window.utils.focus()
|
window.utils.focus()
|
||||||
@ -288,7 +359,7 @@ export function pushNotification(item: RSSItem): AppThunk {
|
|||||||
type: PUSH_NOTIFICATION,
|
type: PUSH_NOTIFICATION,
|
||||||
iid: item._id,
|
iid: item._id,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
source: sourceName
|
source: sourceName,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -300,74 +371,95 @@ export interface InitIntlAction {
|
|||||||
}
|
}
|
||||||
export const initIntlDone = (locale: string): InitIntlAction => ({
|
export const initIntlDone = (locale: string): InitIntlAction => ({
|
||||||
type: INIT_INTL,
|
type: INIT_INTL,
|
||||||
locale: locale
|
locale: locale,
|
||||||
})
|
})
|
||||||
|
|
||||||
export function initIntl(): AppThunk<Promise<void>> {
|
export function initIntl(): AppThunk<Promise<void>> {
|
||||||
return (dispatch) => {
|
return dispatch => {
|
||||||
let locale = getCurrentLocale()
|
let locale = getCurrentLocale()
|
||||||
return intl.init({
|
return intl
|
||||||
currentLocale: locale,
|
.init({
|
||||||
locales: locales,
|
currentLocale: locale,
|
||||||
fallbackLocale: "en-US"
|
locales: locales,
|
||||||
}).then(() => { dispatch(initIntlDone(locale)) })
|
fallbackLocale: "en-US",
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
dispatch(initIntlDone(locale))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initApp(): AppThunk {
|
export function initApp(): AppThunk {
|
||||||
return (dispatch) => {
|
return dispatch => {
|
||||||
document.body.classList.add(window.utils.platform)
|
document.body.classList.add(window.utils.platform)
|
||||||
dispatch(initIntl()).then(async () => {
|
dispatch(initIntl())
|
||||||
if (window.utils.platform === "darwin") initTouchBarWithTexts()
|
.then(async () => {
|
||||||
await dispatch(initSources())
|
if (window.utils.platform === "darwin") initTouchBarWithTexts()
|
||||||
}).then(() => dispatch(initFeeds()))
|
await dispatch(initSources())
|
||||||
.then(async () => {
|
})
|
||||||
dispatch(selectAllArticles())
|
.then(() => dispatch(initFeeds()))
|
||||||
await dispatch(fetchItems())
|
.then(async () => {
|
||||||
}).then(() => {
|
dispatch(selectAllArticles())
|
||||||
dispatch(updateFavicon())
|
await dispatch(fetchItems())
|
||||||
})
|
})
|
||||||
|
.then(() => {
|
||||||
|
dispatch(updateFavicon())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function appReducer(
|
export function appReducer(
|
||||||
state = new AppState(),
|
state = new AppState(),
|
||||||
action: SourceActionTypes | ItemActionTypes | ContextMenuActionTypes | SettingsActionTypes | InitIntlAction
|
action:
|
||||||
| MenuActionTypes | LogMenuActionType | FeedActionTypes | PageActionTypes | SourceGroupActionTypes
|
| SourceActionTypes
|
||||||
|
| ItemActionTypes
|
||||||
|
| ContextMenuActionTypes
|
||||||
|
| SettingsActionTypes
|
||||||
|
| InitIntlAction
|
||||||
|
| MenuActionTypes
|
||||||
|
| LogMenuActionType
|
||||||
|
| FeedActionTypes
|
||||||
|
| PageActionTypes
|
||||||
|
| SourceGroupActionTypes
|
||||||
| ServiceActionTypes
|
| ServiceActionTypes
|
||||||
): AppState {
|
): AppState {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case INIT_INTL: return {
|
case INIT_INTL:
|
||||||
...state,
|
return {
|
||||||
locale: action.locale
|
...state,
|
||||||
}
|
locale: action.locale,
|
||||||
|
}
|
||||||
case INIT_SOURCES:
|
case INIT_SOURCES:
|
||||||
switch (action.status) {
|
switch (action.status) {
|
||||||
case ActionStatus.Success: return {
|
case ActionStatus.Success:
|
||||||
...state,
|
return {
|
||||||
sourceInit: true
|
...state,
|
||||||
}
|
sourceInit: true,
|
||||||
default: return state
|
}
|
||||||
|
default:
|
||||||
|
return state
|
||||||
}
|
}
|
||||||
case ADD_SOURCE:
|
case ADD_SOURCE:
|
||||||
switch (action.status) {
|
switch (action.status) {
|
||||||
case ActionStatus.Request: return {
|
case ActionStatus.Request:
|
||||||
...state,
|
return {
|
||||||
fetchingItems: true,
|
...state,
|
||||||
settings: {
|
fetchingItems: true,
|
||||||
...state.settings,
|
settings: {
|
||||||
changed: true,
|
...state.settings,
|
||||||
saving: true
|
changed: true,
|
||||||
|
saving: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
default:
|
||||||
default: return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
fetchingItems: state.fetchingTotal !== 0,
|
fetchingItems: state.fetchingTotal !== 0,
|
||||||
settings: {
|
settings: {
|
||||||
...state.settings,
|
...state.settings,
|
||||||
saving: action.batch
|
saving: action.batch,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case UPDATE_SOURCE:
|
case UPDATE_SOURCE:
|
||||||
case DELETE_SOURCE:
|
case DELETE_SOURCE:
|
||||||
@ -375,192 +467,243 @@ export function appReducer(
|
|||||||
case ADD_SOURCE_TO_GROUP:
|
case ADD_SOURCE_TO_GROUP:
|
||||||
case REMOVE_SOURCE_FROM_GROUP:
|
case REMOVE_SOURCE_FROM_GROUP:
|
||||||
case REORDER_SOURCE_GROUPS:
|
case REORDER_SOURCE_GROUPS:
|
||||||
case DELETE_SOURCE_GROUP: return {
|
case DELETE_SOURCE_GROUP:
|
||||||
...state,
|
return {
|
||||||
settings: {
|
...state,
|
||||||
...state.settings,
|
settings: {
|
||||||
changed: true
|
...state.settings,
|
||||||
|
changed: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
|
||||||
case INIT_FEEDS:
|
case INIT_FEEDS:
|
||||||
switch (action.status) {
|
switch (action.status) {
|
||||||
case ActionStatus.Request: return state
|
case ActionStatus.Request:
|
||||||
default: return {
|
return state
|
||||||
...state,
|
default:
|
||||||
feedInit: true
|
return {
|
||||||
}
|
...state,
|
||||||
|
feedInit: true,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case SYNC_SERVICE:
|
case SYNC_SERVICE:
|
||||||
switch (action.status) {
|
switch (action.status) {
|
||||||
case ActionStatus.Request: return {
|
case ActionStatus.Request:
|
||||||
...state,
|
return {
|
||||||
syncing: true
|
...state,
|
||||||
}
|
syncing: true,
|
||||||
case ActionStatus.Failure: return {
|
}
|
||||||
...state,
|
case ActionStatus.Failure:
|
||||||
syncing: false,
|
return {
|
||||||
logMenu: {
|
...state,
|
||||||
...state.logMenu,
|
syncing: false,
|
||||||
notify: true,
|
logMenu: {
|
||||||
logs: [...state.logMenu.logs, new AppLog(
|
...state.logMenu,
|
||||||
AppLogType.Failure,
|
notify: true,
|
||||||
intl.get("log.syncFailure"),
|
logs: [
|
||||||
String(action.err)
|
...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:
|
case FETCH_ITEMS:
|
||||||
switch (action.status) {
|
switch (action.status) {
|
||||||
case ActionStatus.Request: return {
|
case ActionStatus.Request:
|
||||||
...state,
|
return {
|
||||||
fetchingItems: true,
|
...state,
|
||||||
fetchingProgress: 0,
|
fetchingItems: true,
|
||||||
fetchingTotal: action.fetchCount
|
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.Failure:
|
||||||
case ActionStatus.Success: return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
fetchingItems: false,
|
logMenu: {
|
||||||
fetchingTotal: 0,
|
...state.logMenu,
|
||||||
logMenu: action.items.length == 0 ? state.logMenu : {
|
notify: !state.logMenu.display,
|
||||||
...state.logMenu,
|
logs: [
|
||||||
logs: [...state.logMenu.logs, new AppLog(
|
...state.logMenu.logs,
|
||||||
AppLogType.Info,
|
new AppLog(
|
||||||
intl.get("log.fetchSuccess", { count: action.items.length })
|
AppLogType.Failure,
|
||||||
)]
|
intl.get("log.fetchFailure", {
|
||||||
|
name: action.errSource.name,
|
||||||
|
}),
|
||||||
|
String(action.err)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
case ActionStatus.Success:
|
||||||
case ActionStatus.Intermediate: return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
fetchingProgress: state.fetchingProgress + 1
|
fetchingItems: false,
|
||||||
}
|
fetchingTotal: 0,
|
||||||
default: return state
|
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) {
|
switch (action.pageType) {
|
||||||
case PageType.AllArticles: return {
|
case PageType.AllArticles:
|
||||||
...state,
|
return {
|
||||||
menu: state.menu && action.keepMenu,
|
...state,
|
||||||
menuKey: ALL,
|
menu: state.menu && action.keepMenu,
|
||||||
title: intl.get("allArticles")
|
menuKey: ALL,
|
||||||
}
|
title: intl.get("allArticles"),
|
||||||
case PageType.Sources: return {
|
}
|
||||||
...state,
|
case PageType.Sources:
|
||||||
menu: state.menu && action.keepMenu,
|
return {
|
||||||
menuKey: action.menuKey,
|
...state,
|
||||||
title: action.title
|
menu: state.menu && action.keepMenu,
|
||||||
}
|
menuKey: action.menuKey,
|
||||||
|
title: action.title,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case CLOSE_CONTEXT_MENU: return {
|
case CLOSE_CONTEXT_MENU:
|
||||||
...state,
|
return {
|
||||||
contextMenu: {
|
...state,
|
||||||
type: ContextMenuType.Hidden
|
contextMenu: {
|
||||||
|
type: ContextMenuType.Hidden,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
case OPEN_ITEM_MENU:
|
||||||
case OPEN_ITEM_MENU: return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
contextMenu: {
|
contextMenu: {
|
||||||
type: ContextMenuType.Item,
|
type: ContextMenuType.Item,
|
||||||
event: action.event,
|
event: action.event,
|
||||||
target: [action.item, action.feedId]
|
target: [action.item, action.feedId],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
case OPEN_TEXT_MENU:
|
||||||
case OPEN_TEXT_MENU: return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
contextMenu: {
|
contextMenu: {
|
||||||
type: ContextMenuType.Text,
|
type: ContextMenuType.Text,
|
||||||
position: action.position,
|
position: action.position,
|
||||||
target: action.item
|
target: action.item,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
case OPEN_VIEW_MENU:
|
||||||
case OPEN_VIEW_MENU: return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
contextMenu: {
|
contextMenu: {
|
||||||
type: state.contextMenu.type === ContextMenuType.View
|
type:
|
||||||
? ContextMenuType.Hidden : ContextMenuType.View,
|
state.contextMenu.type === ContextMenuType.View
|
||||||
event: "#view-toggle"
|
? ContextMenuType.Hidden
|
||||||
|
: ContextMenuType.View,
|
||||||
|
event: "#view-toggle",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
case OPEN_GROUP_MENU:
|
||||||
case OPEN_GROUP_MENU: return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
contextMenu: {
|
contextMenu: {
|
||||||
type: ContextMenuType.Group,
|
type: ContextMenuType.Group,
|
||||||
event: action.event,
|
event: action.event,
|
||||||
target: action.sids
|
target: action.sids,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
case OPEN_IMAGE_MENU:
|
||||||
case OPEN_IMAGE_MENU: return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
contextMenu: {
|
contextMenu: {
|
||||||
type: ContextMenuType.Image,
|
type: ContextMenuType.Image,
|
||||||
position: action.position
|
position: action.position,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
case OPEN_MARK_ALL_MENU:
|
||||||
case OPEN_MARK_ALL_MENU: return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
contextMenu: {
|
contextMenu: {
|
||||||
type: state.contextMenu.type === ContextMenuType.MarkRead
|
type:
|
||||||
? ContextMenuType.Hidden : ContextMenuType.MarkRead,
|
state.contextMenu.type === ContextMenuType.MarkRead
|
||||||
event: "#mark-all-toggle"
|
? ContextMenuType.Hidden
|
||||||
|
: ContextMenuType.MarkRead,
|
||||||
|
event: "#mark-all-toggle",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
case TOGGLE_MENU:
|
||||||
case TOGGLE_MENU: return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
menu: !state.menu
|
menu: !state.menu,
|
||||||
}
|
|
||||||
case SAVE_SETTINGS: return {
|
|
||||||
...state,
|
|
||||||
settings: {
|
|
||||||
...state.settings,
|
|
||||||
display: true,
|
|
||||||
changed: true,
|
|
||||||
saving: !state.settings.saving
|
|
||||||
}
|
}
|
||||||
}
|
case SAVE_SETTINGS:
|
||||||
case TOGGLE_SETTINGS: return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
settings: {
|
settings: {
|
||||||
display: action.open,
|
...state.settings,
|
||||||
changed: false,
|
display: true,
|
||||||
sids: action.sids,
|
changed: true,
|
||||||
saving: false
|
saving: !state.settings.saving,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
case TOGGLE_SETTINGS:
|
||||||
case TOGGLE_LOGS: return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
logMenu: {
|
settings: {
|
||||||
...state.logMenu,
|
display: action.open,
|
||||||
display: !state.logMenu.display,
|
changed: false,
|
||||||
notify: false
|
sids: action.sids,
|
||||||
|
saving: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
case TOGGLE_LOGS:
|
||||||
case PUSH_NOTIFICATION: return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
logMenu: {
|
logMenu: {
|
||||||
...state.logMenu,
|
...state.logMenu,
|
||||||
notify: true,
|
display: !state.logMenu.display,
|
||||||
logs: [
|
notify: false,
|
||||||
...state.logMenu.logs,
|
},
|
||||||
new AppLog(AppLogType.Article, action.title, action.source, action.iid)
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
case PUSH_NOTIFICATION:
|
||||||
default: return state
|
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 * as db from "../db"
|
||||||
import lf from "lovefield"
|
import lf from "lovefield"
|
||||||
import { SourceActionTypes, INIT_SOURCES, ADD_SOURCE, DELETE_SOURCE } from "./source"
|
import {
|
||||||
import { ItemActionTypes, FETCH_ITEMS, RSSItem, TOGGLE_HIDDEN, applyItemReduction } from "./item"
|
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 { ActionStatus, AppThunk, mergeSortedArrays } from "../utils"
|
||||||
import { PageActionTypes, SELECT_PAGE, PageType, APPLY_FILTER } from "./page"
|
import { PageActionTypes, SELECT_PAGE, PageType, APPLY_FILTER } from "./page"
|
||||||
|
|
||||||
@ -23,10 +34,13 @@ export class FeedFilter {
|
|||||||
type: FilterType
|
type: FilterType
|
||||||
search: string
|
search: string
|
||||||
|
|
||||||
constructor(type: FilterType = null, search="") {
|
constructor(type: FilterType = null, search = "") {
|
||||||
if (type === null && (type = window.settings.getFilterType()) === null) {
|
if (
|
||||||
|
type === null &&
|
||||||
|
(type = window.settings.getFilterType()) === null
|
||||||
|
) {
|
||||||
type = FilterType.Default | FilterType.CaseInsensitive
|
type = FilterType.Default | FilterType.CaseInsensitive
|
||||||
}
|
}
|
||||||
this.type = type
|
this.type = type
|
||||||
this.search = search
|
this.search = search
|
||||||
}
|
}
|
||||||
@ -34,17 +48,22 @@ export class FeedFilter {
|
|||||||
static toPredicates(filter: FeedFilter) {
|
static toPredicates(filter: FeedFilter) {
|
||||||
let type = filter.type
|
let type = filter.type
|
||||||
const predicates = new Array<lf.Predicate>()
|
const predicates = new Array<lf.Predicate>()
|
||||||
if (!(type & FilterType.ShowRead)) predicates.push(db.items.hasRead.eq(false))
|
if (!(type & FilterType.ShowRead))
|
||||||
if (!(type & FilterType.ShowNotStarred)) predicates.push(db.items.starred.eq(true))
|
predicates.push(db.items.hasRead.eq(false))
|
||||||
if (!(type & FilterType.ShowHidden)) predicates.push(db.items.hidden.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 !== "") {
|
if (filter.search !== "") {
|
||||||
const flags = (type & FilterType.CaseInsensitive) ? "i" : ""
|
const flags = type & FilterType.CaseInsensitive ? "i" : ""
|
||||||
const regex = RegExp(filter.search, flags)
|
const regex = RegExp(filter.search, flags)
|
||||||
if (type & FilterType.FullSearch) {
|
if (type & FilterType.FullSearch) {
|
||||||
predicates.push(lf.op.or(
|
predicates.push(
|
||||||
db.items.title.match(regex),
|
lf.op.or(
|
||||||
db.items.snippet.match(regex)
|
db.items.title.match(regex),
|
||||||
))
|
db.items.snippet.match(regex)
|
||||||
|
)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
predicates.push(db.items.title.match(regex))
|
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.ShowRead)) flag = flag && !item.hasRead
|
||||||
if (!(type & FilterType.ShowNotStarred)) flag = flag && item.starred
|
if (!(type & FilterType.ShowNotStarred)) flag = flag && item.starred
|
||||||
if (!(type & FilterType.ShowHidden)) flag = flag && !item.hidden
|
if (!(type & FilterType.ShowHidden)) flag = flag && !item.hidden
|
||||||
if (filter.search !== "") {
|
if (filter.search !== "") {
|
||||||
const flags = (type & FilterType.CaseInsensitive) ? "i" : ""
|
const flags = type & FilterType.CaseInsensitive ? "i" : ""
|
||||||
const regex = RegExp(filter.search, flags)
|
const regex = RegExp(filter.search, flags)
|
||||||
if (type & FilterType.FullSearch) {
|
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) {
|
} else if (type & FilterType.CreatorSearch) {
|
||||||
flag = flag && (regex.test(item.creator || ""))
|
flag = flag && regex.test(item.creator || "")
|
||||||
} else {
|
} else {
|
||||||
flag = flag && regex.test(item.title)
|
flag = flag && regex.test(item.title)
|
||||||
}
|
}
|
||||||
@ -87,7 +107,7 @@ export class RSSFeed {
|
|||||||
iids: number[]
|
iids: number[]
|
||||||
filter: FeedFilter
|
filter: FeedFilter
|
||||||
|
|
||||||
constructor (id: string = null, sids=[], filter=null) {
|
constructor(id: string = null, sids = [], filter = null) {
|
||||||
this._id = id
|
this._id = id
|
||||||
this.sids = sids
|
this.sids = sids
|
||||||
this.iids = []
|
this.iids = []
|
||||||
@ -99,12 +119,14 @@ export class RSSFeed {
|
|||||||
static async loadFeed(feed: RSSFeed, skip = 0): Promise<RSSItem[]> {
|
static async loadFeed(feed: RSSFeed, skip = 0): Promise<RSSItem[]> {
|
||||||
const predicates = FeedFilter.toPredicates(feed.filter)
|
const predicates = FeedFilter.toPredicates(feed.filter)
|
||||||
predicates.push(db.items.source.in(feed.sids))
|
predicates.push(db.items.source.in(feed.sids))
|
||||||
return (await db.itemsDB.select().from(db.items).where(
|
return (await db.itemsDB
|
||||||
lf.op.and.apply(null, predicates)
|
.select()
|
||||||
).orderBy(db.items.date, lf.Order.DESC)
|
.from(db.items)
|
||||||
.skip(skip)
|
.where(lf.op.and.apply(null, predicates))
|
||||||
.limit(LOAD_QUANTITY)
|
.orderBy(db.items.date, lf.Order.DESC)
|
||||||
.exec()) as RSSItem[]
|
.skip(skip)
|
||||||
|
.limit(LOAD_QUANTITY)
|
||||||
|
.exec()) as RSSItem[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,13 +160,16 @@ interface loadMoreAction {
|
|||||||
err?
|
err?
|
||||||
}
|
}
|
||||||
|
|
||||||
interface dismissItemsAction{
|
interface dismissItemsAction {
|
||||||
type: typeof DISMISS_ITEMS
|
type: typeof DISMISS_ITEMS
|
||||||
fid: string
|
fid: string
|
||||||
iids: Set<number>
|
iids: Set<number>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FeedActionTypes = initFeedAction | initFeedsAction | loadMoreAction
|
export type FeedActionTypes =
|
||||||
|
| initFeedAction
|
||||||
|
| initFeedsAction
|
||||||
|
| loadMoreAction
|
||||||
| dismissItemsAction
|
| dismissItemsAction
|
||||||
|
|
||||||
export function dismissItems(): AppThunk {
|
export function dismissItems(): AppThunk {
|
||||||
@ -162,7 +187,7 @@ export function dismissItems(): AppThunk {
|
|||||||
dispatch({
|
dispatch({
|
||||||
type: DISMISS_ITEMS,
|
type: DISMISS_ITEMS,
|
||||||
fid: fid,
|
fid: fid,
|
||||||
iids: iids
|
iids: iids,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -170,22 +195,25 @@ export function dismissItems(): AppThunk {
|
|||||||
export function initFeedsRequest(): FeedActionTypes {
|
export function initFeedsRequest(): FeedActionTypes {
|
||||||
return {
|
return {
|
||||||
type: INIT_FEEDS,
|
type: INIT_FEEDS,
|
||||||
status: ActionStatus.Request
|
status: ActionStatus.Request,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export function initFeedsSuccess(): FeedActionTypes {
|
export function initFeedsSuccess(): FeedActionTypes {
|
||||||
return {
|
return {
|
||||||
type: INIT_FEEDS,
|
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 {
|
return {
|
||||||
type: INIT_FEED,
|
type: INIT_FEED,
|
||||||
status: ActionStatus.Success,
|
status: ActionStatus.Success,
|
||||||
items: items,
|
items: items,
|
||||||
feed: feed
|
feed: feed,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,7 +221,7 @@ export function initFeedFailure(err): FeedActionTypes {
|
|||||||
return {
|
return {
|
||||||
type: INIT_FEED,
|
type: INIT_FEED,
|
||||||
status: ActionStatus.Failure,
|
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>>()
|
let promises = new Array<Promise<void>>()
|
||||||
for (let feed of Object.values(getState().feeds)) {
|
for (let feed of Object.values(getState().feeds)) {
|
||||||
if (!feed.loaded || force) {
|
if (!feed.loaded || force) {
|
||||||
let p = RSSFeed.loadFeed(feed).then(items => {
|
let p = RSSFeed.loadFeed(feed)
|
||||||
dispatch(initFeedSuccess(feed, items))
|
.then(items => {
|
||||||
}).catch(err => {
|
dispatch(initFeedSuccess(feed, items))
|
||||||
console.log(err)
|
})
|
||||||
dispatch(initFeedFailure(err))
|
.catch(err => {
|
||||||
})
|
console.log(err)
|
||||||
|
dispatch(initFeedFailure(err))
|
||||||
|
})
|
||||||
promises.push(p)
|
promises.push(p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -222,16 +252,19 @@ export function loadMoreRequest(feed: RSSFeed): FeedActionTypes {
|
|||||||
return {
|
return {
|
||||||
type: LOAD_MORE,
|
type: LOAD_MORE,
|
||||||
status: ActionStatus.Request,
|
status: ActionStatus.Request,
|
||||||
feed: feed
|
feed: feed,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadMoreSuccess(feed: RSSFeed, items: RSSItem[]): FeedActionTypes {
|
export function loadMoreSuccess(
|
||||||
|
feed: RSSFeed,
|
||||||
|
items: RSSItem[]
|
||||||
|
): FeedActionTypes {
|
||||||
return {
|
return {
|
||||||
type: LOAD_MORE,
|
type: LOAD_MORE,
|
||||||
status: ActionStatus.Success,
|
status: ActionStatus.Success,
|
||||||
feed: feed,
|
feed: feed,
|
||||||
items: items
|
items: items,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,7 +273,7 @@ export function loadMoreFailure(feed: RSSFeed, err): FeedActionTypes {
|
|||||||
type: LOAD_MORE,
|
type: LOAD_MORE,
|
||||||
status: ActionStatus.Failure,
|
status: ActionStatus.Failure,
|
||||||
feed: feed,
|
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) {
|
if (feed.loaded && !feed.loading && !feed.allLoaded) {
|
||||||
dispatch(loadMoreRequest(feed))
|
dispatch(loadMoreRequest(feed))
|
||||||
const state = getState()
|
const state = getState()
|
||||||
const skipNum = feed.iids.filter(i => FeedFilter.testItem(feed.filter, state.items[i])).length
|
const skipNum = feed.iids.filter(i =>
|
||||||
return RSSFeed.loadFeed(feed, skipNum).then(items => {
|
FeedFilter.testItem(feed.filter, state.items[i])
|
||||||
dispatch(loadMoreSuccess(feed, items))
|
).length
|
||||||
}).catch(e => {
|
return RSSFeed.loadFeed(feed, skipNum)
|
||||||
console.log(e)
|
.then(items => {
|
||||||
dispatch(loadMoreFailure(feed, e))
|
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(
|
export function feedReducer(
|
||||||
state: FeedState = { [ALL]: new RSSFeed(ALL) },
|
state: FeedState = { [ALL]: new RSSFeed(ALL) },
|
||||||
action: SourceActionTypes | ItemActionTypes | FeedActionTypes | PageActionTypes
|
action:
|
||||||
|
| SourceActionTypes
|
||||||
|
| ItemActionTypes
|
||||||
|
| FeedActionTypes
|
||||||
|
| PageActionTypes
|
||||||
): FeedState {
|
): FeedState {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case INIT_SOURCES:
|
case INIT_SOURCES:
|
||||||
switch (action.status) {
|
switch (action.status) {
|
||||||
case ActionStatus.Success: return {
|
case ActionStatus.Success:
|
||||||
...state,
|
return {
|
||||||
[ALL]: new RSSFeed(ALL, Object.values(action.sources).map(s => s.sid))
|
...state,
|
||||||
}
|
[ALL]: new RSSFeed(
|
||||||
default: return state
|
ALL,
|
||||||
|
Object.values(action.sources).map(s => s.sid)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state
|
||||||
}
|
}
|
||||||
case ADD_SOURCE:
|
case ADD_SOURCE:
|
||||||
switch (action.status) {
|
switch (action.status) {
|
||||||
case ActionStatus.Success: return {
|
case ActionStatus.Success:
|
||||||
...state,
|
return {
|
||||||
[ALL]: new RSSFeed(ALL, [...state[ALL].sids, action.source.sid], state[ALL].filter)
|
...state,
|
||||||
}
|
[ALL]: new RSSFeed(
|
||||||
default: return state
|
ALL,
|
||||||
|
[...state[ALL].sids, action.source.sid],
|
||||||
|
state[ALL].filter
|
||||||
|
),
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state
|
||||||
}
|
}
|
||||||
case DELETE_SOURCE: {
|
case DELETE_SOURCE: {
|
||||||
let nextState = {}
|
let nextState = {}
|
||||||
for (let [id, feed] of Object.entries(state)) {
|
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
|
return nextState
|
||||||
}
|
}
|
||||||
@ -294,7 +352,7 @@ export function feedReducer(
|
|||||||
for (let [id, feed] of Object.entries(state)) {
|
for (let [id, feed] of Object.entries(state)) {
|
||||||
nextState[id] = {
|
nextState[id] = {
|
||||||
...feed,
|
...feed,
|
||||||
filter: action.filter
|
filter: action.filter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nextState
|
return nextState
|
||||||
@ -305,79 +363,102 @@ export function feedReducer(
|
|||||||
let nextState = { ...state }
|
let nextState = { ...state }
|
||||||
for (let feed of Object.values(state)) {
|
for (let feed of Object.values(state)) {
|
||||||
if (feed.loaded) {
|
if (feed.loaded) {
|
||||||
let items = action.items
|
let items = action.items.filter(
|
||||||
.filter(i => feed.sids.includes(i.source) && FeedFilter.testItem(feed.filter, i))
|
i =>
|
||||||
|
feed.sids.includes(i.source) &&
|
||||||
|
FeedFilter.testItem(feed.filter, i)
|
||||||
|
)
|
||||||
if (items.length > 0) {
|
if (items.length > 0) {
|
||||||
let oldItems = feed.iids.map(id => action.itemState[id])
|
let oldItems = feed.iids.map(
|
||||||
let nextItems = mergeSortedArrays(oldItems, items, (a, b) => b.date.getTime() - a.date.getTime())
|
id => action.itemState[id]
|
||||||
nextState[feed._id] = {
|
)
|
||||||
...feed,
|
let nextItems = mergeSortedArrays(
|
||||||
iids: nextItems.map(i => i._id)
|
oldItems,
|
||||||
|
items,
|
||||||
|
(a, b) =>
|
||||||
|
b.date.getTime() - a.date.getTime()
|
||||||
|
)
|
||||||
|
nextState[feed._id] = {
|
||||||
|
...feed,
|
||||||
|
iids: nextItems.map(i => i._id),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nextState
|
return nextState
|
||||||
}
|
}
|
||||||
default: return state
|
default:
|
||||||
|
return state
|
||||||
}
|
}
|
||||||
case DISMISS_ITEMS:
|
case DISMISS_ITEMS:
|
||||||
let nextState = { ...state }
|
let nextState = { ...state }
|
||||||
let feed = state[action.fid]
|
let feed = state[action.fid]
|
||||||
nextState[action.fid] = {
|
nextState[action.fid] = {
|
||||||
...feed,
|
...feed,
|
||||||
iids: feed.iids.filter(iid => !action.iids.has(iid))
|
iids: feed.iids.filter(iid => !action.iids.has(iid)),
|
||||||
}
|
}
|
||||||
return nextState
|
return nextState
|
||||||
case INIT_FEED:
|
case INIT_FEED:
|
||||||
switch (action.status) {
|
switch (action.status) {
|
||||||
case ActionStatus.Success: return {
|
case ActionStatus.Success:
|
||||||
...state,
|
return {
|
||||||
[action.feed._id]: {
|
...state,
|
||||||
...action.feed,
|
[action.feed._id]: {
|
||||||
loaded: true,
|
...action.feed,
|
||||||
allLoaded: action.items.length < LOAD_QUANTITY,
|
loaded: true,
|
||||||
iids: action.items.map(i => i._id)
|
allLoaded: action.items.length < LOAD_QUANTITY,
|
||||||
|
iids: action.items.map(i => i._id),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
default:
|
||||||
default: return state
|
return state
|
||||||
}
|
}
|
||||||
case LOAD_MORE:
|
case LOAD_MORE:
|
||||||
switch (action.status) {
|
switch (action.status) {
|
||||||
case ActionStatus.Request: return {
|
case ActionStatus.Request:
|
||||||
...state,
|
return {
|
||||||
[action.feed._id] : {
|
...state,
|
||||||
...action.feed,
|
[action.feed._id]: {
|
||||||
loading: true
|
...action.feed,
|
||||||
|
loading: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
case ActionStatus.Success:
|
||||||
case ActionStatus.Success: return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
[action.feed._id] : {
|
[action.feed._id]: {
|
||||||
...action.feed,
|
...action.feed,
|
||||||
loading: false,
|
loading: false,
|
||||||
allLoaded: action.items.length < LOAD_QUANTITY,
|
allLoaded: action.items.length < LOAD_QUANTITY,
|
||||||
iids: [...action.feed.iids, ...action.items.map(i => i._id)]
|
iids: [
|
||||||
|
...action.feed.iids,
|
||||||
|
...action.items.map(i => i._id),
|
||||||
|
],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
case ActionStatus.Failure:
|
||||||
case ActionStatus.Failure: return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
[action.feed._id] : {
|
[action.feed._id]: {
|
||||||
...action.feed,
|
...action.feed,
|
||||||
loading: false
|
loading: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
default:
|
||||||
default: return state
|
return state
|
||||||
}
|
}
|
||||||
case TOGGLE_HIDDEN: {
|
case TOGGLE_HIDDEN: {
|
||||||
let nextItem = applyItemReduction(action.item, action.type)
|
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) {
|
if (filteredFeeds.length > 0) {
|
||||||
let nextState = { ...state }
|
let nextState = { ...state }
|
||||||
for (let feed of filteredFeeds) {
|
for (let feed of filteredFeeds) {
|
||||||
nextState[feed._id] = {
|
nextState[feed._id] = {
|
||||||
...feed,
|
...feed,
|
||||||
iids: feed.iids.filter(id => id != nextItem._id)
|
iids: feed.iids.filter(id => id != nextItem._id),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nextState
|
return nextState
|
||||||
@ -387,20 +468,30 @@ export function feedReducer(
|
|||||||
}
|
}
|
||||||
case SELECT_PAGE:
|
case SELECT_PAGE:
|
||||||
switch (action.pageType) {
|
switch (action.pageType) {
|
||||||
case PageType.Sources: return {
|
case PageType.Sources:
|
||||||
...state,
|
return {
|
||||||
[SOURCE]: new RSSFeed(SOURCE, action.sids, action.filter)
|
...state,
|
||||||
}
|
[SOURCE]: new RSSFeed(
|
||||||
case PageType.AllArticles: return action.init ? {
|
SOURCE,
|
||||||
...state,
|
action.sids,
|
||||||
[ALL]: {
|
action.filter
|
||||||
...state[ALL],
|
),
|
||||||
loaded: false,
|
|
||||||
filter: action.filter
|
|
||||||
}
|
}
|
||||||
} : state
|
case PageType.AllArticles:
|
||||||
default: return state
|
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 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 { SourceGroup } from "../../schema-types"
|
||||||
import { ActionStatus, AppThunk, domParser } from "../utils"
|
import { ActionStatus, AppThunk, domParser } from "../utils"
|
||||||
import { saveSettings } from "./app"
|
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 CREATE_SOURCE_GROUP = "CREATE_SOURCE_GROUP"
|
||||||
export const ADD_SOURCE_TO_GROUP = "ADD_SOURCE_TO_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"
|
export const TOGGLE_GROUP_EXPANSION = "TOGGLE_GROUP_EXPANSION"
|
||||||
|
|
||||||
interface CreateSourceGroupAction {
|
interface CreateSourceGroupAction {
|
||||||
type: typeof CREATE_SOURCE_GROUP,
|
type: typeof CREATE_SOURCE_GROUP
|
||||||
group: SourceGroup
|
group: SourceGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AddSourceToGroupAction {
|
interface AddSourceToGroupAction {
|
||||||
type: typeof ADD_SOURCE_TO_GROUP,
|
type: typeof ADD_SOURCE_TO_GROUP
|
||||||
groupIndex: number,
|
groupIndex: number
|
||||||
sid: number
|
sid: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RemoveSourceFromGroupAction {
|
interface RemoveSourceFromGroupAction {
|
||||||
type: typeof REMOVE_SOURCE_FROM_GROUP,
|
type: typeof REMOVE_SOURCE_FROM_GROUP
|
||||||
groupIndex: number,
|
groupIndex: number
|
||||||
sids: number[]
|
sids: number[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateSourceGroupAction {
|
interface UpdateSourceGroupAction {
|
||||||
type: typeof UPDATE_SOURCE_GROUP,
|
type: typeof UPDATE_SOURCE_GROUP
|
||||||
groupIndex: number,
|
groupIndex: number
|
||||||
group: SourceGroup
|
group: SourceGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReorderSourceGroupsAction {
|
interface ReorderSourceGroupsAction {
|
||||||
type: typeof REORDER_SOURCE_GROUPS,
|
type: typeof REORDER_SOURCE_GROUPS
|
||||||
groups: SourceGroup[]
|
groups: SourceGroup[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DeleteSourceGroupAction {
|
interface DeleteSourceGroupAction {
|
||||||
type: typeof DELETE_SOURCE_GROUP,
|
type: typeof DELETE_SOURCE_GROUP
|
||||||
groupIndex: number
|
groupIndex: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ToggleGroupExpansionAction {
|
interface ToggleGroupExpansionAction {
|
||||||
type: typeof TOGGLE_GROUP_EXPANSION,
|
type: typeof TOGGLE_GROUP_EXPANSION
|
||||||
groupIndex: number
|
groupIndex: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SourceGroupActionTypes = CreateSourceGroupAction | AddSourceToGroupAction
|
export type SourceGroupActionTypes =
|
||||||
| RemoveSourceFromGroupAction | UpdateSourceGroupAction | ReorderSourceGroupsAction
|
| CreateSourceGroupAction
|
||||||
| DeleteSourceGroupAction | ToggleGroupExpansionAction
|
| AddSourceToGroupAction
|
||||||
|
| RemoveSourceFromGroupAction
|
||||||
|
| UpdateSourceGroupAction
|
||||||
|
| ReorderSourceGroupsAction
|
||||||
|
| DeleteSourceGroupAction
|
||||||
|
| ToggleGroupExpansionAction
|
||||||
|
|
||||||
export function createSourceGroupDone(group: SourceGroup): SourceGroupActionTypes {
|
export function createSourceGroupDone(
|
||||||
|
group: SourceGroup
|
||||||
|
): SourceGroupActionTypes {
|
||||||
return {
|
return {
|
||||||
type: CREATE_SOURCE_GROUP,
|
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 {
|
return {
|
||||||
type: ADD_SOURCE_TO_GROUP,
|
type: ADD_SOURCE_TO_GROUP,
|
||||||
groupIndex: groupIndex,
|
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 {
|
return {
|
||||||
type: REMOVE_SOURCE_FROM_GROUP,
|
type: REMOVE_SOURCE_FROM_GROUP,
|
||||||
groupIndex: groupIndex,
|
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) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(removeSourceFromGroupDone(groupIndex, sids))
|
dispatch(removeSourceFromGroupDone(groupIndex, sids))
|
||||||
window.settings.saveGroups(getState().groups)
|
window.settings.saveGroups(getState().groups)
|
||||||
@ -112,7 +139,7 @@ export function removeSourceFromGroup(groupIndex: number, sids: number[]): AppTh
|
|||||||
function deleteSourceGroupDone(groupIndex: number): SourceGroupActionTypes {
|
function deleteSourceGroupDone(groupIndex: number): SourceGroupActionTypes {
|
||||||
return {
|
return {
|
||||||
type: DELETE_SOURCE_GROUP,
|
type: DELETE_SOURCE_GROUP,
|
||||||
groupIndex: groupIndex
|
groupIndex: groupIndex,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,7 +154,7 @@ function updateSourceGroupDone(group: SourceGroup): SourceGroupActionTypes {
|
|||||||
return {
|
return {
|
||||||
type: UPDATE_SOURCE_GROUP,
|
type: UPDATE_SOURCE_GROUP,
|
||||||
groupIndex: group.index,
|
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 {
|
return {
|
||||||
type: REORDER_SOURCE_GROUPS,
|
type: REORDER_SOURCE_GROUPS,
|
||||||
groups: groups
|
groups: groups,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,7 +185,7 @@ export function toggleGroupExpansion(groupIndex: number): AppThunk {
|
|||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: TOGGLE_GROUP_EXPANSION,
|
type: TOGGLE_GROUP_EXPANSION,
|
||||||
groupIndex: groupIndex
|
groupIndex: groupIndex,
|
||||||
})
|
})
|
||||||
window.settings.saveGroups(getState().groups)
|
window.settings.saveGroups(getState().groups)
|
||||||
}
|
}
|
||||||
@ -167,16 +196,18 @@ export function fixBrokenGroups(sources: SourceState): AppThunk {
|
|||||||
const { groups } = getState()
|
const { groups } = getState()
|
||||||
const sids = new Set(Object.values(sources).map(s => s.sid))
|
const sids = new Set(Object.values(sources).map(s => s.sid))
|
||||||
let isBroken = false
|
let isBroken = false
|
||||||
const newGroups: SourceGroup[] = groups.map(group => {
|
const newGroups: SourceGroup[] = groups
|
||||||
const newGroup: SourceGroup = {
|
.map(group => {
|
||||||
...group,
|
const newGroup: SourceGroup = {
|
||||||
sids: group.sids.filter(sid => sids.delete(sid))
|
...group,
|
||||||
}
|
sids: group.sids.filter(sid => sids.delete(sid)),
|
||||||
if (newGroup.sids.length !== group.sids.length) {
|
}
|
||||||
isBroken = true
|
if (newGroup.sids.length !== group.sids.length) {
|
||||||
}
|
isBroken = true
|
||||||
return newGroup
|
}
|
||||||
}).filter(group => group.isMultiple || group.sids.length > 0)
|
return newGroup
|
||||||
|
})
|
||||||
|
.filter(group => group.isMultiple || group.sids.length > 0)
|
||||||
if (isBroken || sids.size > 0) {
|
if (isBroken || sids.size > 0) {
|
||||||
for (let sid of sids) {
|
for (let sid of sids) {
|
||||||
newGroups.push(new SourceGroup([sid]))
|
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 url = outline.getAttribute("xmlUrl")
|
||||||
let name = outline.getAttribute("text") || outline.getAttribute("title")
|
let name = outline.getAttribute("text") || outline.getAttribute("title")
|
||||||
if (url) {
|
if (url) {
|
||||||
@ -197,12 +230,16 @@ function outlineToSource(outline: Element): [ReturnType<typeof addSource>, strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function importOPML(): AppThunk {
|
export function importOPML(): AppThunk {
|
||||||
return async (dispatch) => {
|
return async dispatch => {
|
||||||
const filters = [{ name: intl.get("sources.opmlFile"), extensions: ["xml", "opml"] }]
|
const filters = [
|
||||||
|
{ name: intl.get("sources.opmlFile"), extensions: ["xml", "opml"] },
|
||||||
|
]
|
||||||
window.utils.showOpenDialog(filters).then(data => {
|
window.utils.showOpenDialog(filters).then(data => {
|
||||||
if (data) {
|
if (data) {
|
||||||
dispatch(saveSettings())
|
dispatch(saveSettings())
|
||||||
let doc = domParser.parseFromString(data, "text/xml").getElementsByTagName("body")
|
let doc = domParser
|
||||||
|
.parseFromString(data, "text/xml")
|
||||||
|
.getElementsByTagName("body")
|
||||||
if (doc.length == 0) {
|
if (doc.length == 0) {
|
||||||
dispatch(saveSettings())
|
dispatch(saveSettings())
|
||||||
return
|
return
|
||||||
@ -210,43 +247,60 @@ export function importOPML(): AppThunk {
|
|||||||
let parseError = doc[0].getElementsByTagName("parsererror")
|
let parseError = doc[0].getElementsByTagName("parsererror")
|
||||||
if (parseError.length > 0) {
|
if (parseError.length > 0) {
|
||||||
dispatch(saveSettings())
|
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
|
return
|
||||||
}
|
}
|
||||||
let sources: [ReturnType<typeof addSource>, number, string][] = []
|
let sources: [ReturnType<typeof addSource>, number, string][] =
|
||||||
|
[]
|
||||||
let errors: [string, any][] = []
|
let errors: [string, any][] = []
|
||||||
for (let el of doc[0].children) {
|
for (let el of doc[0].children) {
|
||||||
if (el.getAttribute("type") === "rss") {
|
if (el.getAttribute("type") === "rss") {
|
||||||
let source = outlineToSource(el)
|
let source = outlineToSource(el)
|
||||||
if (source) sources.push([source[0], -1, source[1]])
|
if (source) sources.push([source[0], -1, source[1]])
|
||||||
} else if (el.hasAttribute("text") || el.hasAttribute("title")) {
|
} else if (
|
||||||
let groupName = el.getAttribute("text") || el.getAttribute("title")
|
el.hasAttribute("text") ||
|
||||||
|
el.hasAttribute("title")
|
||||||
|
) {
|
||||||
|
let groupName =
|
||||||
|
el.getAttribute("text") || el.getAttribute("title")
|
||||||
let gid = dispatch(createSourceGroup(groupName))
|
let gid = dispatch(createSourceGroup(groupName))
|
||||||
for (let child of el.children) {
|
for (let child of el.children) {
|
||||||
let source = outlineToSource(child)
|
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))
|
dispatch(fetchItemsRequest(sources.length))
|
||||||
let promises = sources.map(([s, gid, url]) => {
|
let promises = sources.map(([s, gid, url]) => {
|
||||||
return dispatch(s).then(sid => {
|
return dispatch(s)
|
||||||
if (sid !== null && gid > -1) dispatch(addSourceToGroup(gid, sid))
|
.then(sid => {
|
||||||
}).catch(err => {
|
if (sid !== null && gid > -1)
|
||||||
errors.push([url, err])
|
dispatch(addSourceToGroup(gid, sid))
|
||||||
}).finally(() => {
|
})
|
||||||
dispatch(fetchItemsIntermediate())
|
.catch(err => {
|
||||||
})
|
errors.push([url, err])
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
dispatch(fetchItemsIntermediate())
|
||||||
|
})
|
||||||
})
|
})
|
||||||
Promise.allSettled(promises).then(() => {
|
Promise.allSettled(promises).then(() => {
|
||||||
dispatch(fetchItemsSuccess([], {}))
|
dispatch(fetchItemsSuccess([], {}))
|
||||||
dispatch(saveSettings())
|
dispatch(saveSettings())
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
window.utils.showErrorBox(
|
window.utils.showErrorBox(
|
||||||
intl.get("sources.errorImport", { count: errors.length }),
|
intl.get("sources.errorImport", {
|
||||||
errors.map(e => {
|
count: errors.length,
|
||||||
return e[0] + "\n" + String(e[1])
|
}),
|
||||||
}).join("\n")
|
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 {
|
export function exportOPML(): AppThunk {
|
||||||
return (_, getState) => {
|
return (_, getState) => {
|
||||||
const filters = [{ name: intl.get("sources.opmlFile"), extensions: ["opml"] }]
|
const filters = [
|
||||||
window.utils.showSaveDialog(filters, "*/Fluent_Reader_Export.opml").then(write => {
|
{ name: intl.get("sources.opmlFile"), extensions: ["opml"] },
|
||||||
if (write) {
|
]
|
||||||
let state = getState()
|
window.utils
|
||||||
let xml = domParser.parseFromString(
|
.showSaveDialog(filters, "*/Fluent_Reader_Export.opml")
|
||||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><opml version=\"1.0\"><head><title>Fluent Reader Export</title></head><body></body></opml>",
|
.then(write => {
|
||||||
"text/xml"
|
if (write) {
|
||||||
)
|
let state = getState()
|
||||||
let body = xml.getElementsByTagName("body")[0]
|
let xml = domParser.parseFromString(
|
||||||
for (let group of state.groups) {
|
'<?xml version="1.0" encoding="UTF-8"?><opml version="1.0"><head><title>Fluent Reader Export</title></head><body></body></opml>',
|
||||||
if (group.isMultiple) {
|
"text/xml"
|
||||||
let outline = xml.createElement("outline")
|
)
|
||||||
outline.setAttribute("text", group.name)
|
let body = xml.getElementsByTagName("body")[0]
|
||||||
outline.setAttribute("title", group.name)
|
for (let group of state.groups) {
|
||||||
for (let sid of group.sids) {
|
if (group.isMultiple) {
|
||||||
outline.appendChild(sourceToOutline(state.sources[sid], xml))
|
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(),
|
state = window.settings.loadGroups(),
|
||||||
action: SourceActionTypes | SourceGroupActionTypes
|
action: SourceActionTypes | SourceGroupActionTypes
|
||||||
): GroupState {
|
): GroupState {
|
||||||
switch(action.type) {
|
switch (action.type) {
|
||||||
case ADD_SOURCE:
|
case ADD_SOURCE:
|
||||||
switch (action.status) {
|
switch (action.status) {
|
||||||
case ActionStatus.Success: return [
|
case ActionStatus.Success:
|
||||||
...state,
|
return [...state, new SourceGroup([action.source.sid])]
|
||||||
new SourceGroup([action.source.sid])
|
default:
|
||||||
]
|
return state
|
||||||
default: return state
|
|
||||||
}
|
}
|
||||||
case DELETE_SOURCE: return [
|
case DELETE_SOURCE:
|
||||||
...state.map(group => ({
|
return [
|
||||||
...group,
|
...state
|
||||||
sids: group.sids.filter(sid => sid != action.source.sid)
|
.map(group => ({
|
||||||
})).filter(g => g.isMultiple || g.sids.length == 1)
|
...group,
|
||||||
]
|
sids: group.sids.filter(
|
||||||
case CREATE_SOURCE_GROUP: return [ ...state, action.group ]
|
sid => sid != action.source.sid
|
||||||
case ADD_SOURCE_TO_GROUP: return state.map((g, i) => ({
|
),
|
||||||
...g,
|
}))
|
||||||
sids: i == action.groupIndex
|
.filter(g => g.isMultiple || g.sids.length == 1),
|
||||||
? [ ...g.sids.filter(sid => sid !== action.sid), action.sid ]
|
]
|
||||||
: g.sids.filter(sid => sid !== action.sid)
|
case CREATE_SOURCE_GROUP:
|
||||||
})).filter(g => g.isMultiple || g.sids.length > 0)
|
return [...state, action.group]
|
||||||
case REMOVE_SOURCE_FROM_GROUP: return [
|
case ADD_SOURCE_TO_GROUP:
|
||||||
...state.slice(0, action.groupIndex),
|
return state
|
||||||
{
|
.map((g, i) => ({
|
||||||
...state[action.groupIndex],
|
...g,
|
||||||
sids: state[action.groupIndex].sids.filter(sid => !action.sids.includes(sid))
|
sids:
|
||||||
},
|
i == action.groupIndex
|
||||||
...action.sids.map(sid => new SourceGroup([sid])),
|
? [
|
||||||
...state.slice(action.groupIndex + 1)
|
...g.sids.filter(sid => sid !== action.sid),
|
||||||
]
|
action.sid,
|
||||||
case UPDATE_SOURCE_GROUP: return [
|
]
|
||||||
...state.slice(0, action.groupIndex),
|
: g.sids.filter(sid => sid !== action.sid),
|
||||||
action.group,
|
}))
|
||||||
...state.slice(action.groupIndex + 1)
|
.filter(g => g.isMultiple || g.sids.length > 0)
|
||||||
]
|
case REMOVE_SOURCE_FROM_GROUP:
|
||||||
case REORDER_SOURCE_GROUPS: return action.groups
|
return [
|
||||||
case DELETE_SOURCE_GROUP: return [
|
...state.slice(0, action.groupIndex),
|
||||||
...state.slice(0, action.groupIndex),
|
{
|
||||||
...state[action.groupIndex].sids.map(sid => new SourceGroup([sid])),
|
...state[action.groupIndex],
|
||||||
...state.slice(action.groupIndex + 1)
|
sids: state[action.groupIndex].sids.filter(
|
||||||
]
|
sid => !action.sids.includes(sid)
|
||||||
case TOGGLE_GROUP_EXPANSION: return state.map((g, i) => i == action.groupIndex ? ({
|
),
|
||||||
...g,
|
},
|
||||||
expanded: !g.expanded
|
...action.sids.map(sid => new SourceGroup([sid])),
|
||||||
}) : g)
|
...state.slice(action.groupIndex + 1),
|
||||||
default: return state
|
]
|
||||||
|
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 * as db from "../db"
|
||||||
import lf from "lovefield"
|
import lf from "lovefield"
|
||||||
import intl from "react-intl-universal"
|
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 { 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 Parser from "@yang991178/rss-parser"
|
||||||
import { pushNotification, setupAutoFetch, SettingsActionTypes, FREE_MEMORY } from "./app"
|
import {
|
||||||
import { getServiceHooks, syncWithService, ServiceActionTypes, SYNC_LOCAL_ITEMS } from "./service"
|
pushNotification,
|
||||||
|
setupAutoFetch,
|
||||||
|
SettingsActionTypes,
|
||||||
|
FREE_MEMORY,
|
||||||
|
} from "./app"
|
||||||
|
import {
|
||||||
|
getServiceHooks,
|
||||||
|
syncWithService,
|
||||||
|
ServiceActionTypes,
|
||||||
|
SYNC_LOCAL_ITEMS,
|
||||||
|
} from "./service"
|
||||||
|
|
||||||
export class RSSItem {
|
export class RSSItem {
|
||||||
_id: number
|
_id: number
|
||||||
@ -25,7 +48,7 @@ export class RSSItem {
|
|||||||
notify: boolean
|
notify: boolean
|
||||||
serviceRef?: string
|
serviceRef?: string
|
||||||
|
|
||||||
constructor (item: Parser.Item, source: RSSSource) {
|
constructor(item: Parser.Item, source: RSSSource) {
|
||||||
for (let field of ["title", "link", "creator"]) {
|
for (let field of ["title", "link", "creator"]) {
|
||||||
const content = item[field]
|
const content = item[field]
|
||||||
if (content && typeof content !== "string") delete item[field]
|
if (content && typeof content !== "string") delete item[field]
|
||||||
@ -54,25 +77,34 @@ export class RSSItem {
|
|||||||
item.content = parsed.content || ""
|
item.content = parsed.content || ""
|
||||||
item.snippet = htmlDecode(parsed.contentSnippet || "")
|
item.snippet = htmlDecode(parsed.contentSnippet || "")
|
||||||
}
|
}
|
||||||
if (parsed.thumb) {
|
if (parsed.thumb) {
|
||||||
item.thumb = parsed.thumb
|
item.thumb = parsed.thumb
|
||||||
} else if (parsed.image && parsed.image.$ && parsed.image.$.url) {
|
} else if (parsed.image && parsed.image.$ && parsed.image.$.url) {
|
||||||
item.thumb = parsed.image.$.url
|
item.thumb = parsed.image.$.url
|
||||||
} else if (parsed.image && typeof parsed.image === "string") {
|
} else if (parsed.image && typeof parsed.image === "string") {
|
||||||
item.thumb = parsed.image
|
item.thumb = parsed.image
|
||||||
} else if (parsed.mediaContent) {
|
} 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 (images.length > 0) item.thumb = images[0].$.url
|
||||||
}
|
}
|
||||||
if (!item.thumb) {
|
if (!item.thumb) {
|
||||||
let dom = domParser.parseFromString(item.content, "text/html")
|
let dom = domParser.parseFromString(item.content, "text/html")
|
||||||
let baseEl = dom.createElement('base')
|
let baseEl = dom.createElement("base")
|
||||||
baseEl.setAttribute('href', item.link.split("/").slice(0, 3).join("/"))
|
baseEl.setAttribute(
|
||||||
|
"href",
|
||||||
|
item.link.split("/").slice(0, 3).join("/")
|
||||||
|
)
|
||||||
dom.head.append(baseEl)
|
dom.head.append(baseEl)
|
||||||
let img = dom.querySelector("img")
|
let img = dom.querySelector("img")
|
||||||
if (img && img.src) item.thumb = img.src
|
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
|
delete item.thumb
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -105,7 +137,7 @@ interface MarkReadAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface MarkAllReadAction {
|
interface MarkAllReadAction {
|
||||||
type: typeof MARK_ALL_READ,
|
type: typeof MARK_ALL_READ
|
||||||
sids: number[]
|
sids: number[]
|
||||||
time?: number
|
time?: number
|
||||||
before?: boolean
|
before?: boolean
|
||||||
@ -126,23 +158,31 @@ interface ToggleHiddenAction {
|
|||||||
item: RSSItem
|
item: RSSItem
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ItemActionTypes = FetchItemsAction | MarkReadAction | MarkAllReadAction | MarkUnreadAction
|
export type ItemActionTypes =
|
||||||
| ToggleStarredAction | ToggleHiddenAction
|
| FetchItemsAction
|
||||||
|
| MarkReadAction
|
||||||
|
| MarkAllReadAction
|
||||||
|
| MarkUnreadAction
|
||||||
|
| ToggleStarredAction
|
||||||
|
| ToggleHiddenAction
|
||||||
|
|
||||||
export function fetchItemsRequest(fetchCount = 0): ItemActionTypes {
|
export function fetchItemsRequest(fetchCount = 0): ItemActionTypes {
|
||||||
return {
|
return {
|
||||||
type: FETCH_ITEMS,
|
type: FETCH_ITEMS,
|
||||||
status: ActionStatus.Request,
|
status: ActionStatus.Request,
|
||||||
fetchCount: fetchCount
|
fetchCount: fetchCount,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchItemsSuccess(items: RSSItem[], itemState: ItemState): ItemActionTypes {
|
export function fetchItemsSuccess(
|
||||||
|
items: RSSItem[],
|
||||||
|
itemState: ItemState
|
||||||
|
): ItemActionTypes {
|
||||||
return {
|
return {
|
||||||
type: FETCH_ITEMS,
|
type: FETCH_ITEMS,
|
||||||
status: ActionStatus.Success,
|
status: ActionStatus.Success,
|
||||||
items: items,
|
items: items,
|
||||||
itemState: itemState
|
itemState: itemState,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,41 +191,65 @@ export function fetchItemsFailure(source: RSSSource, err): ItemActionTypes {
|
|||||||
type: FETCH_ITEMS,
|
type: FETCH_ITEMS,
|
||||||
status: ActionStatus.Failure,
|
status: ActionStatus.Failure,
|
||||||
errSource: source,
|
errSource: source,
|
||||||
err: err
|
err: err,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchItemsIntermediate(): ItemActionTypes {
|
export function fetchItemsIntermediate(): ItemActionTypes {
|
||||||
return {
|
return {
|
||||||
type: FETCH_ITEMS,
|
type: FETCH_ITEMS,
|
||||||
status: ActionStatus.Intermediate
|
status: ActionStatus.Intermediate,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function insertItems(items: RSSItem[]): Promise<RSSItem[]> {
|
export async function insertItems(items: RSSItem[]): Promise<RSSItem[]> {
|
||||||
items.sort((a, b) => a.date.getTime() - b.date.getTime())
|
items.sort((a, b) => a.date.getTime() - b.date.getTime())
|
||||||
const rows = items.map(item => db.items.createRow(item))
|
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) => {
|
return async (dispatch, getState) => {
|
||||||
let promises = new Array<Promise<RSSItem[]>>()
|
let promises = new Array<Promise<RSSItem[]>>()
|
||||||
const initState = getState()
|
const initState = getState()
|
||||||
if (!initState.app.fetchingItems && !initState.app.syncing) {
|
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))
|
await dispatch(syncWithService(background))
|
||||||
let timenow = new Date().getTime()
|
let timenow = new Date().getTime()
|
||||||
const sourcesState = getState().sources
|
const sourcesState = getState().sources
|
||||||
let sources = (sids === null)
|
let sources =
|
||||||
? Object.values(sourcesState).filter(s => {
|
sids === null
|
||||||
let last = s.lastFetched ? s.lastFetched.getTime() : 0
|
? Object.values(sourcesState).filter(s => {
|
||||||
return !s.serviceRef && ((last > timenow) || (last + (s.fetchFrequency || 0) * 60000 <= timenow))
|
let last = s.lastFetched ? s.lastFetched.getTime() : 0
|
||||||
})
|
return (
|
||||||
: sids.map(sid => sourcesState[sid]).filter(s => !s.serviceRef)
|
!s.serviceRef &&
|
||||||
|
(last > timenow ||
|
||||||
|
last + (s.fetchFrequency || 0) * 60000 <=
|
||||||
|
timenow)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
: sids
|
||||||
|
.map(sid => sourcesState[sid])
|
||||||
|
.filter(s => !s.serviceRef)
|
||||||
for (let source of sources) {
|
for (let source of sources) {
|
||||||
let promise = RSSSource.fetchItems(source)
|
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()))
|
promise.finally(() => dispatch(fetchItemsIntermediate()))
|
||||||
promises.push(promise)
|
promises.push(promise)
|
||||||
}
|
}
|
||||||
@ -201,48 +265,60 @@ export function fetchItems(background = false, sids: number[] = null): AppThunk<
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
insertItems(items)
|
insertItems(items)
|
||||||
.then(inserted => {
|
.then(inserted => {
|
||||||
dispatch(fetchItemsSuccess(inserted.reverse(), getState().items))
|
dispatch(
|
||||||
resolve()
|
fetchItemsSuccess(
|
||||||
if (background) {
|
inserted.reverse(),
|
||||||
for (let item of inserted) {
|
getState().items
|
||||||
if (item.notify) {
|
)
|
||||||
dispatch(pushNotification(item))
|
)
|
||||||
|
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) {
|
dispatch(setupAutoFetch())
|
||||||
window.utils.requestAttention()
|
})
|
||||||
}
|
.catch(err => {
|
||||||
} else {
|
dispatch(fetchItemsSuccess([], getState().items))
|
||||||
dispatch(dismissItems())
|
window.utils.showErrorBox(
|
||||||
}
|
"A database error has occurred.",
|
||||||
dispatch(setupAutoFetch())
|
String(err)
|
||||||
})
|
)
|
||||||
.catch(err => {
|
console.log(err)
|
||||||
dispatch(fetchItemsSuccess([], getState().items))
|
reject(err)
|
||||||
window.utils.showErrorBox("A database error has occurred.", String(err))
|
})
|
||||||
console.log(err)
|
|
||||||
reject(err)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const markReadDone = (item: RSSItem): ItemActionTypes => ({
|
const markReadDone = (item: RSSItem): ItemActionTypes => ({
|
||||||
type: MARK_READ,
|
type: MARK_READ,
|
||||||
item: item
|
item: item,
|
||||||
})
|
})
|
||||||
|
|
||||||
const markUnreadDone = (item: RSSItem): ItemActionTypes => ({
|
const markUnreadDone = (item: RSSItem): ItemActionTypes => ({
|
||||||
type: MARK_UNREAD,
|
type: MARK_UNREAD,
|
||||||
item: item
|
item: item,
|
||||||
})
|
})
|
||||||
|
|
||||||
export function markRead(item: RSSItem): AppThunk {
|
export function markRead(item: RSSItem): AppThunk {
|
||||||
return (dispatch) => {
|
return dispatch => {
|
||||||
if (!item.hasRead) {
|
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))
|
dispatch(markReadDone(item))
|
||||||
if (item.serviceRef) {
|
if (item.serviceRef) {
|
||||||
dispatch(dispatch(getServiceHooks()).markRead?.(item))
|
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) => {
|
return async (dispatch, getState) => {
|
||||||
let state = getState()
|
let state = getState()
|
||||||
if (sids === null) {
|
if (sids === null) {
|
||||||
let feed = state.feeds[state.page.feedId]
|
let feed = state.feeds[state.page.feedId]
|
||||||
sids = feed.sids
|
sids = feed.sids
|
||||||
}
|
}
|
||||||
const action = dispatch(getServiceHooks()).markAllRead?.(sids, date, before)
|
const action = dispatch(getServiceHooks()).markAllRead?.(
|
||||||
|
sids,
|
||||||
|
date,
|
||||||
|
before
|
||||||
|
)
|
||||||
if (action) await dispatch(action)
|
if (action) await dispatch(action)
|
||||||
const predicates: lf.Predicate[] = [
|
const predicates: lf.Predicate[] = [
|
||||||
db.items.source.in(sids),
|
db.items.source.in(sids),
|
||||||
db.items.hasRead.eq(false)
|
db.items.hasRead.eq(false),
|
||||||
]
|
]
|
||||||
if (date) {
|
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 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) {
|
if (date) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: MARK_ALL_READ,
|
type: MARK_ALL_READ,
|
||||||
sids: sids,
|
sids: sids,
|
||||||
time: date.getTime(),
|
time: date.getTime(),
|
||||||
before: before
|
before: before,
|
||||||
})
|
})
|
||||||
dispatch(updateUnreadCounts())
|
dispatch(updateUnreadCounts())
|
||||||
} else {
|
} else {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: MARK_ALL_READ,
|
type: MARK_ALL_READ,
|
||||||
sids: sids
|
sids: sids,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function markUnread(item: RSSItem): AppThunk {
|
export function markUnread(item: RSSItem): AppThunk {
|
||||||
return (dispatch) => {
|
return dispatch => {
|
||||||
if (item.hasRead) {
|
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))
|
dispatch(markUnreadDone(item))
|
||||||
if (item.serviceRef) {
|
if (item.serviceRef) {
|
||||||
dispatch(dispatch(getServiceHooks()).markUnread?.(item))
|
dispatch(dispatch(getServiceHooks()).markUnread?.(item))
|
||||||
@ -298,14 +392,18 @@ export function markUnread(item: RSSItem): AppThunk {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleStarredDone = (item: RSSItem): ItemActionTypes => ({
|
const toggleStarredDone = (item: RSSItem): ItemActionTypes => ({
|
||||||
type: TOGGLE_STARRED,
|
type: TOGGLE_STARRED,
|
||||||
item: item
|
item: item,
|
||||||
})
|
})
|
||||||
|
|
||||||
export function toggleStarred(item: RSSItem): AppThunk {
|
export function toggleStarred(item: RSSItem): AppThunk {
|
||||||
return (dispatch) => {
|
return dispatch => {
|
||||||
db.itemsDB.update(db.items).where(db.items._id.eq(item._id)).set(db.items.starred, !item.starred).exec()
|
db.itemsDB
|
||||||
|
.update(db.items)
|
||||||
|
.where(db.items._id.eq(item._id))
|
||||||
|
.set(db.items.starred, !item.starred)
|
||||||
|
.exec()
|
||||||
dispatch(toggleStarredDone(item))
|
dispatch(toggleStarredDone(item))
|
||||||
if (item.serviceRef) {
|
if (item.serviceRef) {
|
||||||
const hooks = dispatch(getServiceHooks())
|
const hooks = dispatch(getServiceHooks())
|
||||||
@ -315,34 +413,42 @@ export function toggleStarred(item: RSSItem): AppThunk {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleHiddenDone = (item: RSSItem): ItemActionTypes => ({
|
const toggleHiddenDone = (item: RSSItem): ItemActionTypes => ({
|
||||||
type: TOGGLE_HIDDEN,
|
type: TOGGLE_HIDDEN,
|
||||||
item: item
|
item: item,
|
||||||
})
|
})
|
||||||
|
|
||||||
export function toggleHidden(item: RSSItem): AppThunk {
|
export function toggleHidden(item: RSSItem): AppThunk {
|
||||||
return (dispatch) => {
|
return dispatch => {
|
||||||
db.itemsDB.update(db.items).where(db.items._id.eq(item._id)).set(db.items.hidden, !item.hidden).exec()
|
db.itemsDB
|
||||||
|
.update(db.items)
|
||||||
|
.where(db.items._id.eq(item._id))
|
||||||
|
.set(db.items.hidden, !item.hidden)
|
||||||
|
.exec()
|
||||||
dispatch(toggleHiddenDone(item))
|
dispatch(toggleHiddenDone(item))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function itemShortcuts(item: RSSItem, e: KeyboardEvent): AppThunk {
|
export function itemShortcuts(item: RSSItem, e: KeyboardEvent): AppThunk {
|
||||||
return (dispatch) => {
|
return dispatch => {
|
||||||
if (e.metaKey) return
|
if (e.metaKey) return
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case "m": case "M":
|
case "m":
|
||||||
|
case "M":
|
||||||
if (item.hasRead) dispatch(markUnread(item))
|
if (item.hasRead) dispatch(markUnread(item))
|
||||||
else dispatch(markRead(item))
|
else dispatch(markRead(item))
|
||||||
break
|
break
|
||||||
case "b": case "B":
|
case "b":
|
||||||
|
case "B":
|
||||||
if (!item.hasRead) dispatch(markRead(item))
|
if (!item.hasRead) dispatch(markRead(item))
|
||||||
window.utils.openExternal(item.link, platformCtrl(e))
|
window.utils.openExternal(item.link, platformCtrl(e))
|
||||||
break
|
break
|
||||||
case "s": case "S":
|
case "s":
|
||||||
|
case "S":
|
||||||
dispatch(toggleStarred(item))
|
dispatch(toggleStarred(item))
|
||||||
break
|
break
|
||||||
case "h": case "H":
|
case "h":
|
||||||
|
case "H":
|
||||||
if (!item.hasRead && !item.hidden) dispatch(markRead(item))
|
if (!item.hasRead && !item.hidden) dispatch(markRead(item))
|
||||||
dispatch(toggleHidden(item))
|
dispatch(toggleHidden(item))
|
||||||
break
|
break
|
||||||
@ -372,7 +478,11 @@ export function applyItemReduction(item: RSSItem, type: string) {
|
|||||||
|
|
||||||
export function itemReducer(
|
export function itemReducer(
|
||||||
state: ItemState = {},
|
state: ItemState = {},
|
||||||
action: ItemActionTypes | FeedActionTypes | ServiceActionTypes | SettingsActionTypes
|
action:
|
||||||
|
| ItemActionTypes
|
||||||
|
| FeedActionTypes
|
||||||
|
| ServiceActionTypes
|
||||||
|
| SettingsActionTypes
|
||||||
): ItemState {
|
): ItemState {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case FETCH_ITEMS:
|
case FETCH_ITEMS:
|
||||||
@ -382,9 +492,10 @@ export function itemReducer(
|
|||||||
for (let i of action.items) {
|
for (let i of action.items) {
|
||||||
newMap[i._id] = i
|
newMap[i._id] = i
|
||||||
}
|
}
|
||||||
return {...newMap, ...state}
|
return { ...newMap, ...state }
|
||||||
}
|
}
|
||||||
default: return state
|
default:
|
||||||
|
return state
|
||||||
}
|
}
|
||||||
case MARK_UNREAD:
|
case MARK_UNREAD:
|
||||||
case MARK_READ:
|
case MARK_READ:
|
||||||
@ -392,7 +503,10 @@ export function itemReducer(
|
|||||||
case TOGGLE_HIDDEN: {
|
case TOGGLE_HIDDEN: {
|
||||||
return {
|
return {
|
||||||
...state,
|
...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: {
|
case MARK_ALL_READ: {
|
||||||
@ -400,13 +514,15 @@ export function itemReducer(
|
|||||||
let sids = new Set(action.sids)
|
let sids = new Set(action.sids)
|
||||||
for (let item of Object.values(state)) {
|
for (let item of Object.values(state)) {
|
||||||
if (sids.has(item.source) && !item.hasRead) {
|
if (sids.has(item.source) && !item.hasRead) {
|
||||||
if (!action.time || (action.before
|
if (
|
||||||
? item.date.getTime() <= action.time
|
!action.time ||
|
||||||
: item.date.getTime() >= action.time)
|
(action.before
|
||||||
|
? item.date.getTime() <= action.time
|
||||||
|
: item.date.getTime() >= action.time)
|
||||||
) {
|
) {
|
||||||
nextState[item._id] = {
|
nextState[item._id] = {
|
||||||
...item,
|
...item,
|
||||||
hasRead: true
|
hasRead: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -423,7 +539,8 @@ export function itemReducer(
|
|||||||
}
|
}
|
||||||
return nextState
|
return nextState
|
||||||
}
|
}
|
||||||
default: return state
|
default:
|
||||||
|
return state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case SYNC_LOCAL_ITEMS: {
|
case SYNC_LOCAL_ITEMS: {
|
||||||
@ -445,6 +562,7 @@ export function itemReducer(
|
|||||||
}
|
}
|
||||||
return nextState
|
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 { getWindowBreakpoint, AppThunk, ActionStatus } from "../utils"
|
||||||
import { RSSItem, markRead } from "./item"
|
import { RSSItem, markRead } from "./item"
|
||||||
import { SourceActionTypes, DELETE_SOURCE } from "./source"
|
import { SourceActionTypes, DELETE_SOURCE } from "./source"
|
||||||
@ -15,7 +24,9 @@ export const APPLY_FILTER = "APPLY_FILTER"
|
|||||||
export const TOGGLE_SEARCH = "TOGGLE_SEARCH"
|
export const TOGGLE_SEARCH = "TOGGLE_SEARCH"
|
||||||
|
|
||||||
export enum PageType {
|
export enum PageType {
|
||||||
AllArticles, Sources, Page
|
AllArticles,
|
||||||
|
Sources,
|
||||||
|
Page,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SelectPageAction {
|
interface SelectPageAction {
|
||||||
@ -50,11 +61,21 @@ interface ApplyFilterAction {
|
|||||||
filter: FeedFilter
|
filter: FeedFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DismissItemAction { type: typeof DISMISS_ITEM }
|
interface DismissItemAction {
|
||||||
interface ToggleSearchAction { type: typeof TOGGLE_SEARCH }
|
type: typeof DISMISS_ITEM
|
||||||
|
}
|
||||||
|
interface ToggleSearchAction {
|
||||||
|
type: typeof TOGGLE_SEARCH
|
||||||
|
}
|
||||||
|
|
||||||
export type PageActionTypes = SelectPageAction | SwitchViewAction | ShowItemAction
|
export type PageActionTypes =
|
||||||
| DismissItemAction | ApplyFilterAction | ToggleSearchAction | SetViewConfigsAction
|
| SelectPageAction
|
||||||
|
| SwitchViewAction
|
||||||
|
| ShowItemAction
|
||||||
|
| DismissItemAction
|
||||||
|
| ApplyFilterAction
|
||||||
|
| ToggleSearchAction
|
||||||
|
| SetViewConfigsAction
|
||||||
|
|
||||||
export function selectAllArticles(init = false): AppThunk {
|
export function selectAllArticles(init = false): AppThunk {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
@ -63,12 +84,16 @@ export function selectAllArticles(init = false): AppThunk {
|
|||||||
keepMenu: getWindowBreakpoint(),
|
keepMenu: getWindowBreakpoint(),
|
||||||
filter: getState().page.filter,
|
filter: getState().page.filter,
|
||||||
pageType: PageType.AllArticles,
|
pageType: PageType.AllArticles,
|
||||||
init: init
|
init: init,
|
||||||
} as PageActionTypes)
|
} 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) => {
|
return (dispatch, getState) => {
|
||||||
if (getState().app.menuKey !== menuKey) {
|
if (getState().app.menuKey !== menuKey) {
|
||||||
dispatch({
|
dispatch({
|
||||||
@ -79,16 +104,16 @@ export function selectSources(sids: number[], menuKey: string, title: string): A
|
|||||||
sids: sids,
|
sids: sids,
|
||||||
menuKey: menuKey,
|
menuKey: menuKey,
|
||||||
title: title,
|
title: title,
|
||||||
init: true
|
init: true,
|
||||||
} as PageActionTypes)
|
} as PageActionTypes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function switchView(viewType: ViewType): PageActionTypes {
|
export function switchView(viewType: ViewType): PageActionTypes {
|
||||||
return {
|
return {
|
||||||
type: SWITCH_VIEW,
|
type: SWITCH_VIEW,
|
||||||
viewType: viewType
|
viewType: viewType,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,7 +122,7 @@ export function setViewConfigs(configs: ViewConfigs): AppThunk {
|
|||||||
window.settings.setViewConfigs(getState().page.viewType, configs)
|
window.settings.setViewConfigs(getState().page.viewType, configs)
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "SET_VIEW_CONFIGS",
|
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 {
|
export function showItem(feedId: string, item: RSSItem): AppThunk {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const state = 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({
|
dispatch({
|
||||||
type: SHOW_ITEM,
|
type: SHOW_ITEM,
|
||||||
feedId: feedId,
|
feedId: feedId,
|
||||||
item: item
|
item: item,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -128,15 +156,17 @@ export const dismissItem = (): PageActionTypes => ({ type: DISMISS_ITEM })
|
|||||||
export const toggleSearch = (): AppThunk => {
|
export const toggleSearch = (): AppThunk => {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
let state = getState()
|
let state = getState()
|
||||||
dispatch(({ type: TOGGLE_SEARCH }))
|
dispatch({ type: TOGGLE_SEARCH })
|
||||||
if (!getWindowBreakpoint() && state.app.menu) {
|
if (!getWindowBreakpoint() && state.app.menu) {
|
||||||
dispatch(toggleMenu())
|
dispatch(toggleMenu())
|
||||||
}
|
}
|
||||||
if (state.page.searchOn) {
|
if (state.page.searchOn) {
|
||||||
dispatch(applyFilter({
|
dispatch(
|
||||||
...state.page.filter,
|
applyFilter({
|
||||||
search: ""
|
...state.page.filter,
|
||||||
}))
|
search: "",
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -153,7 +183,9 @@ export function showOffsetItem(offset: number): AppThunk {
|
|||||||
if (itemIndex < 0) {
|
if (itemIndex < 0) {
|
||||||
let item = state.items[itemId]
|
let item = state.items[itemId]
|
||||||
let prevs = feed.iids
|
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)
|
.filter(([i, _]) => i.date > item.date)
|
||||||
if (prevs.length > 0) {
|
if (prevs.length > 0) {
|
||||||
let prev = prevs[0]
|
let prev = prevs[0]
|
||||||
@ -171,12 +203,12 @@ export function showOffsetItem(offset: number): AppThunk {
|
|||||||
dispatch(markRead(item))
|
dispatch(markRead(item))
|
||||||
dispatch(showItem(feedId, item))
|
dispatch(showItem(feedId, item))
|
||||||
return
|
return
|
||||||
} else if (!feed.allLoaded){
|
} else if (!feed.allLoaded) {
|
||||||
dispatch(loadMore(feed)).then(() => {
|
dispatch(loadMore(feed))
|
||||||
dispatch(showOffsetItem(offset))
|
.then(() => {
|
||||||
}).catch(() =>
|
dispatch(showOffsetItem(offset))
|
||||||
dispatch(dismissItem())
|
})
|
||||||
)
|
.catch(() => dispatch(dismissItem()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -186,13 +218,14 @@ export function showOffsetItem(offset: number): AppThunk {
|
|||||||
|
|
||||||
const applyFilterDone = (filter: FeedFilter): PageActionTypes => ({
|
const applyFilterDone = (filter: FeedFilter): PageActionTypes => ({
|
||||||
type: APPLY_FILTER,
|
type: APPLY_FILTER,
|
||||||
filter: filter
|
filter: filter,
|
||||||
})
|
})
|
||||||
|
|
||||||
function applyFilter(filter: FeedFilter): AppThunk {
|
function applyFilter(filter: FeedFilter): AppThunk {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const oldFilterType = getState().page.filter.type
|
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(applyFilterDone(filter))
|
||||||
dispatch(initFeeds(true))
|
dispatch(initFeeds(true))
|
||||||
}
|
}
|
||||||
@ -204,10 +237,12 @@ export function switchFilter(filter: FilterType): AppThunk {
|
|||||||
let oldType = oldFilter.type
|
let oldType = oldFilter.type
|
||||||
let newType = filter | (oldType & FilterType.Toggles)
|
let newType = filter | (oldType & FilterType.Toggles)
|
||||||
if (oldType != newType) {
|
if (oldType != newType) {
|
||||||
dispatch(applyFilter({
|
dispatch(
|
||||||
...oldFilter,
|
applyFilter({
|
||||||
type: newType
|
...oldFilter,
|
||||||
}))
|
type: newType,
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -224,17 +259,21 @@ export function performSearch(query: string): AppThunk {
|
|||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
let state = getState()
|
let state = getState()
|
||||||
if (state.page.searchOn) {
|
if (state.page.searchOn) {
|
||||||
dispatch(applyFilter({
|
dispatch(
|
||||||
...state.page.filter,
|
applyFilter({
|
||||||
search: query
|
...state.page.filter,
|
||||||
}))
|
search: query,
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PageState {
|
export class PageState {
|
||||||
viewType = window.settings.getDefaultView()
|
viewType = window.settings.getDefaultView()
|
||||||
viewConfigs = window.settings.getViewConfigs(window.settings.getDefaultView())
|
viewConfigs = window.settings.getViewConfigs(
|
||||||
|
window.settings.getDefaultView()
|
||||||
|
)
|
||||||
filter = new FeedFilter()
|
filter = new FeedFilter()
|
||||||
feedId = ALL
|
feedId = ALL
|
||||||
itemId = null as number
|
itemId = null as number
|
||||||
@ -249,54 +288,71 @@ export function pageReducer(
|
|||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case SELECT_PAGE:
|
case SELECT_PAGE:
|
||||||
switch (action.pageType) {
|
switch (action.pageType) {
|
||||||
case PageType.AllArticles: return {
|
case PageType.AllArticles:
|
||||||
...state,
|
return {
|
||||||
feedId: ALL,
|
...state,
|
||||||
itemId: null
|
feedId: ALL,
|
||||||
}
|
itemId: null,
|
||||||
case PageType.Sources: return {
|
}
|
||||||
...state,
|
case PageType.Sources:
|
||||||
feedId: SOURCE,
|
return {
|
||||||
itemId: null
|
...state,
|
||||||
}
|
feedId: SOURCE,
|
||||||
default: return state
|
itemId: null,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state
|
||||||
}
|
}
|
||||||
case SWITCH_VIEW: return {
|
case SWITCH_VIEW:
|
||||||
...state,
|
return {
|
||||||
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,
|
...state,
|
||||||
itemId: (action.feed._id === state.feedId && action.items.filter(i => i._id === state.itemId).length === 0)
|
viewType: action.viewType,
|
||||||
? null : state.itemId
|
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 DELETE_SOURCE:
|
||||||
case DISMISS_ITEM: return {
|
case DISMISS_ITEM:
|
||||||
...state,
|
return {
|
||||||
itemId: null
|
...state,
|
||||||
}
|
itemId: null,
|
||||||
case TOGGLE_SEARCH: return {
|
}
|
||||||
...state,
|
case TOGGLE_SEARCH:
|
||||||
searchOn: !state.searchOn
|
return {
|
||||||
}
|
...state,
|
||||||
default: return state
|
searchOn: !state.searchOn,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,8 @@ import { FeedFilter, FilterType } from "./feed"
|
|||||||
import { RSSItem } from "./item"
|
import { RSSItem } from "./item"
|
||||||
|
|
||||||
export const enum ItemAction {
|
export const enum ItemAction {
|
||||||
Read = "r",
|
Read = "r",
|
||||||
Star = "s",
|
Star = "s",
|
||||||
Hide = "h",
|
Hide = "h",
|
||||||
Notify = "n",
|
Notify = "n",
|
||||||
}
|
}
|
||||||
@ -49,7 +49,12 @@ export class SourceRule {
|
|||||||
match: boolean
|
match: boolean
|
||||||
actions: RuleActions
|
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.filter = new FeedFilter(filter, regex)
|
||||||
this.match = match
|
this.match = match
|
||||||
this.actions = RuleActions.fromKeys(actions)
|
this.actions = RuleActions.fromKeys(actions)
|
||||||
@ -69,4 +74,4 @@ export class SourceRule {
|
|||||||
this.apply(rule, item)
|
this.apply(rule, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,15 @@ import { SyncService, ServiceConfigs } from "../../schema-types"
|
|||||||
import { AppThunk, ActionStatus } from "../utils"
|
import { AppThunk, ActionStatus } from "../utils"
|
||||||
import { RSSItem, insertItems, fetchItemsSuccess } from "./item"
|
import { RSSItem, insertItems, fetchItemsSuccess } from "./item"
|
||||||
import { saveSettings, pushNotification } from "./app"
|
import { saveSettings, pushNotification } from "./app"
|
||||||
import { deleteSource, updateUnreadCounts, RSSSource, insertSource, addSourceSuccess,
|
import {
|
||||||
updateSource, updateFavicon } from "./source"
|
deleteSource,
|
||||||
|
updateUnreadCounts,
|
||||||
|
RSSSource,
|
||||||
|
insertSource,
|
||||||
|
addSourceSuccess,
|
||||||
|
updateSource,
|
||||||
|
updateFavicon,
|
||||||
|
} from "./source"
|
||||||
import { createSourceGroup, addSourceToGroup } from "./group"
|
import { createSourceGroup, addSourceToGroup } from "./group"
|
||||||
|
|
||||||
import { feverServiceHooks } from "./services/fever"
|
import { feverServiceHooks } from "./services/fever"
|
||||||
@ -20,19 +27,26 @@ export interface ServiceHooks {
|
|||||||
syncItems?: () => AppThunk<Promise<[Set<string>, Set<string>]>>
|
syncItems?: () => AppThunk<Promise<[Set<string>, Set<string>]>>
|
||||||
markRead?: (item: RSSItem) => AppThunk
|
markRead?: (item: RSSItem) => AppThunk
|
||||||
markUnread?: (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
|
star?: (item: RSSItem) => AppThunk
|
||||||
unstar?: (item: RSSItem) => AppThunk
|
unstar?: (item: RSSItem) => AppThunk
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getServiceHooksFromType(type: SyncService): ServiceHooks {
|
export function getServiceHooksFromType(type: SyncService): ServiceHooks {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case SyncService.Fever: return feverServiceHooks
|
case SyncService.Fever:
|
||||||
case SyncService.Feedbin: return feedbinServiceHooks
|
return feverServiceHooks
|
||||||
|
case SyncService.Feedbin:
|
||||||
|
return feedbinServiceHooks
|
||||||
case SyncService.GReader:
|
case SyncService.GReader:
|
||||||
case SyncService.Inoreader:
|
case SyncService.Inoreader:
|
||||||
return gReaderServiceHooks
|
return gReaderServiceHooks
|
||||||
default: return {}
|
default:
|
||||||
|
return {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,7 +63,7 @@ export function syncWithService(background = false): AppThunk<Promise<void>> {
|
|||||||
try {
|
try {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: SYNC_SERVICE,
|
type: SYNC_SERVICE,
|
||||||
status: ActionStatus.Request
|
status: ActionStatus.Request,
|
||||||
})
|
})
|
||||||
if (hooks.reauthenticate) await dispatch(reauthenticate(hooks))
|
if (hooks.reauthenticate) await dispatch(reauthenticate(hooks))
|
||||||
await dispatch(updateSources(hooks.updateSources))
|
await dispatch(updateSources(hooks.updateSources))
|
||||||
@ -57,14 +71,14 @@ export function syncWithService(background = false): AppThunk<Promise<void>> {
|
|||||||
await dispatch(fetchItems(hooks.fetchItems, background))
|
await dispatch(fetchItems(hooks.fetchItems, background))
|
||||||
dispatch({
|
dispatch({
|
||||||
type: SYNC_SERVICE,
|
type: SYNC_SERVICE,
|
||||||
status: ActionStatus.Success
|
status: ActionStatus.Success,
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
dispatch({
|
dispatch({
|
||||||
type: SYNC_SERVICE,
|
type: SYNC_SERVICE,
|
||||||
status: ActionStatus.Failure,
|
status: ActionStatus.Failure,
|
||||||
err: err
|
err: err,
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
if (getState().app.settings.saving) dispatch(saveSettings())
|
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>> {
|
function updateSources(
|
||||||
return async (dispatch, getState) => {
|
hook: ServiceHooks["updateSources"]
|
||||||
|
): AppThunk<Promise<void>> {
|
||||||
|
return async (dispatch, getState) => {
|
||||||
const [sources, groupsMap] = await dispatch(hook())
|
const [sources, groupsMap] = await dispatch(hook())
|
||||||
const existing = new Map<string, RSSSource>()
|
const existing = new Map<string, RSSSource>()
|
||||||
for (let source of Object.values(getState().sources)) {
|
for (let source of Object.values(getState().sources)) {
|
||||||
@ -93,17 +109,19 @@ function updateSources(hook: ServiceHooks["updateSources"]): AppThunk<Promise<vo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const forceSettings = () => {
|
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)) {
|
if (existing.has(s.serviceRef)) {
|
||||||
const doc = existing.get(s.serviceRef)
|
const doc = existing.get(s.serviceRef)
|
||||||
existing.delete(s.serviceRef)
|
existing.delete(s.serviceRef)
|
||||||
return doc
|
return doc
|
||||||
} else {
|
} else {
|
||||||
const docs = (await db.sourcesDB.select().from(db.sources).where(
|
const docs = (await db.sourcesDB
|
||||||
db.sources.url.eq(s.url)
|
.select()
|
||||||
).exec()) as RSSSource[]
|
.from(db.sources)
|
||||||
|
.where(db.sources.url.eq(s.url))
|
||||||
|
.exec()) as RSSSource[]
|
||||||
if (docs.length === 0) {
|
if (docs.length === 0) {
|
||||||
// Create a new source
|
// Create a new source
|
||||||
forceSettings()
|
forceSettings()
|
||||||
@ -120,7 +138,11 @@ function updateSources(hook: ServiceHooks["updateSources"]): AppThunk<Promise<vo
|
|||||||
doc.serviceRef = s.serviceRef
|
doc.serviceRef = s.serviceRef
|
||||||
doc.unreadCount = 0
|
doc.unreadCount = 0
|
||||||
await dispatch(updateSource(doc))
|
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
|
return doc
|
||||||
} else {
|
} else {
|
||||||
return docs[0]
|
return docs[0]
|
||||||
@ -138,7 +160,9 @@ function updateSources(hook: ServiceHooks["updateSources"]): AppThunk<Promise<vo
|
|||||||
forceSettings()
|
forceSettings()
|
||||||
for (let source of sourcesResults) {
|
for (let source of sourcesResults) {
|
||||||
if (groupsMap.has(source.serviceRef)) {
|
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))
|
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>> {
|
function syncItems(hook: ServiceHooks["syncItems"]): AppThunk<Promise<void>> {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
const state = getState()
|
const state = getState()
|
||||||
const [unreadRefs, starredRefs] = await dispatch(hook())
|
const [unreadRefs, starredRefs] = await dispatch(hook())
|
||||||
const unreadCopy = new Set(unreadRefs)
|
const unreadCopy = new Set(unreadRefs)
|
||||||
const starredCopy = new Set(starredRefs)
|
const starredCopy = new Set(starredRefs)
|
||||||
const rows = await db.itemsDB.select(
|
const rows = await db.itemsDB
|
||||||
db.items.serviceRef, db.items.hasRead, db.items.starred
|
.select(db.items.serviceRef, db.items.hasRead, db.items.starred)
|
||||||
).from(db.items).where(lf.op.and(
|
.from(db.items)
|
||||||
db.items.serviceRef.isNotNull(),
|
.where(
|
||||||
lf.op.or(db.items.hasRead.eq(false), db.items.starred.eq(true))
|
lf.op.and(
|
||||||
)).exec()
|
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>()
|
const updates = new Array<lf.query.Update>()
|
||||||
for (let row of rows) {
|
for (let row of rows) {
|
||||||
const serviceRef = row["serviceRef"]
|
const serviceRef = row["serviceRef"]
|
||||||
if (row["hasRead"] === false && !unreadRefs.delete(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)) {
|
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) {
|
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) {
|
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) {
|
if (updates.length > 0) {
|
||||||
await db.itemsDB.createTransaction().exec(updates)
|
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) => {
|
return async (dispatch, getState) => {
|
||||||
const [items, configs] = await dispatch(hook())
|
const [items, configs] = await dispatch(hook())
|
||||||
if (items.length > 0) {
|
if (items.length > 0) {
|
||||||
@ -218,9 +272,11 @@ export function removeService(): AppThunk<Promise<void>> {
|
|||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
dispatch(saveSettings())
|
dispatch(saveSettings())
|
||||||
const state = getState()
|
const state = getState()
|
||||||
const promises = Object.values(state.sources).filter(s => s.serviceRef).map(async s => {
|
const promises = Object.values(state.sources)
|
||||||
await dispatch(deleteSource(s, true))
|
.filter(s => s.serviceRef)
|
||||||
})
|
.map(async s => {
|
||||||
|
await dispatch(deleteSource(s, true))
|
||||||
|
})
|
||||||
await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
dispatch(saveServiceConfigs({ type: SyncService.None }))
|
dispatch(saveServiceConfigs({ type: SyncService.None }))
|
||||||
dispatch(saveSettings())
|
dispatch(saveSettings())
|
||||||
@ -248,23 +304,29 @@ interface SyncLocalItemsAction {
|
|||||||
starredIds: Set<string>
|
starredIds: Set<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServiceActionTypes = SaveServiceConfigsAction | SyncWithServiceAction | SyncLocalItemsAction
|
export type ServiceActionTypes =
|
||||||
|
| SaveServiceConfigsAction
|
||||||
|
| SyncWithServiceAction
|
||||||
|
| SyncLocalItemsAction
|
||||||
|
|
||||||
export function saveServiceConfigs(configs: ServiceConfigs): AppThunk {
|
export function saveServiceConfigs(configs: ServiceConfigs): AppThunk {
|
||||||
return (dispatch) => {
|
return dispatch => {
|
||||||
window.settings.setServiceConfigs(configs)
|
window.settings.setServiceConfigs(configs)
|
||||||
dispatch({
|
dispatch({
|
||||||
type: SAVE_SERVICE_CONFIGS,
|
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 {
|
return {
|
||||||
type: SYNC_LOCAL_ITEMS,
|
type: SYNC_LOCAL_ITEMS,
|
||||||
unreadIds: unread,
|
unreadIds: unread,
|
||||||
starredIds: starred
|
starredIds: starred,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,7 +335,9 @@ export function serviceReducer(
|
|||||||
action: ServiceActionTypes
|
action: ServiceActionTypes
|
||||||
): ServiceConfigs {
|
): ServiceConfigs {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case SAVE_SERVICE_CONFIGS: return action.configs
|
case SAVE_SERVICE_CONFIGS:
|
||||||
default: return state
|
return action.configs
|
||||||
|
default:
|
||||||
|
return state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,13 +20,24 @@ export interface FeedbinConfigs extends ServiceConfigs {
|
|||||||
|
|
||||||
async function fetchAPI(configs: FeedbinConfigs, params: string) {
|
async function fetchAPI(configs: FeedbinConfigs, params: string) {
|
||||||
const headers = new Headers()
|
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 })
|
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()
|
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")
|
headers.set("Content-Type", "application/json; charset=utf-8")
|
||||||
const promises = new Array<Promise<Response>>()
|
const promises = new Array<Promise<Response>>()
|
||||||
while (refs.length > 0) {
|
while (refs.length > 0) {
|
||||||
@ -36,16 +47,18 @@ async function markItems(configs: FeedbinConfigs, type: string, method: string,
|
|||||||
}
|
}
|
||||||
const bodyObject: any = {}
|
const bodyObject: any = {}
|
||||||
bodyObject[`${type}_entries`] = batch
|
bodyObject[`${type}_entries`] = batch
|
||||||
promises.push(fetch(configs.endpoint + type + "_entries.json", {
|
promises.push(
|
||||||
method: method,
|
fetch(configs.endpoint + type + "_entries.json", {
|
||||||
headers: headers,
|
method: method,
|
||||||
body: JSON.stringify(bodyObject)
|
headers: headers,
|
||||||
}))
|
body: JSON.stringify(bodyObject),
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return await Promise.all(promises)
|
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 = {
|
export const feedbinServiceHooks: ServiceHooks = {
|
||||||
authenticate: async (configs: FeedbinConfigs) => {
|
authenticate: async (configs: FeedbinConfigs) => {
|
||||||
@ -89,13 +102,17 @@ export const feedbinServiceHooks: ServiceHooks = {
|
|||||||
syncItems: () => async (_, getState) => {
|
syncItems: () => async (_, getState) => {
|
||||||
const configs = getState().service as FeedbinConfigs
|
const configs = getState().service as FeedbinConfigs
|
||||||
const [unreadResponse, starredResponse] = await Promise.all([
|
const [unreadResponse, starredResponse] = await Promise.all([
|
||||||
fetchAPI(configs, "unread_entries.json"),
|
fetchAPI(configs, "unread_entries.json"),
|
||||||
fetchAPI(configs, "starred_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 unread = await unreadResponse.json()
|
||||||
const starred = await starredResponse.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) => {
|
fetchItems: () => async (_, getState) => {
|
||||||
@ -108,10 +125,17 @@ export const feedbinServiceHooks: ServiceHooks = {
|
|||||||
let lastFetched: any[]
|
let lastFetched: any[]
|
||||||
do {
|
do {
|
||||||
try {
|
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()
|
if (response.status !== 200) throw APIError()
|
||||||
lastFetched = await response.json()
|
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)
|
min = lastFetched.reduce((m, n) => Math.min(m, n.id), min)
|
||||||
page += 1
|
page += 1
|
||||||
} catch {
|
} catch {
|
||||||
@ -119,10 +143,14 @@ export const feedbinServiceHooks: ServiceHooks = {
|
|||||||
}
|
}
|
||||||
} while (
|
} while (
|
||||||
min > configs.lastId &&
|
min > configs.lastId &&
|
||||||
lastFetched && lastFetched.length >= 125 &&
|
lastFetched &&
|
||||||
|
lastFetched.length >= 125 &&
|
||||||
items.length < configs.fetchLimit
|
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) {
|
if (items.length > 0) {
|
||||||
const fidMap = new Map<string, RSSSource>()
|
const fidMap = new Map<string, RSSSource>()
|
||||||
for (let source of Object.values(state.sources)) {
|
for (let source of Object.values(state.sources)) {
|
||||||
@ -131,10 +159,11 @@ export const feedbinServiceHooks: ServiceHooks = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const [unreadResponse, starredResponse] = await Promise.all([
|
const [unreadResponse, starredResponse] = await Promise.all([
|
||||||
fetchAPI(configs, "unread_entries.json"),
|
fetchAPI(configs, "unread_entries.json"),
|
||||||
fetchAPI(configs, "starred_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 unread: Set<number> = new Set(await unreadResponse.json())
|
||||||
const starred: Set<number> = new Set(await starredResponse.json())
|
const starred: Set<number> = new Set(await starredResponse.json())
|
||||||
const parsedItems = new Array<RSSItem>()
|
const parsedItems = new Array<RSSItem>()
|
||||||
@ -160,8 +189,11 @@ export const feedbinServiceHooks: ServiceHooks = {
|
|||||||
if (i.images && i.images.original_url) {
|
if (i.images && i.images.original_url) {
|
||||||
item.thumb = i.images.original_url
|
item.thumb = i.images.original_url
|
||||||
} else {
|
} else {
|
||||||
let baseEl = dom.createElement('base')
|
let baseEl = dom.createElement("base")
|
||||||
baseEl.setAttribute('href', item.link.split("/").slice(0, 3).join("/"))
|
baseEl.setAttribute(
|
||||||
|
"href",
|
||||||
|
item.link.split("/").slice(0, 3).join("/")
|
||||||
|
)
|
||||||
dom.head.append(baseEl)
|
dom.head.append(baseEl)
|
||||||
let img = dom.querySelector("img")
|
let img = dom.querySelector("img")
|
||||||
if (img && img.src) item.thumb = img.src
|
if (img && img.src) item.thumb = img.src
|
||||||
@ -169,9 +201,19 @@ export const feedbinServiceHooks: ServiceHooks = {
|
|||||||
// Apply rules and sync back to the service
|
// Apply rules and sync back to the service
|
||||||
if (source.rules) SourceRule.applyAll(source.rules, item)
|
if (source.rules) SourceRule.applyAll(source.rules, item)
|
||||||
if (unread.has(i.id) === item.hasRead)
|
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))
|
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)
|
parsedItems.push(item)
|
||||||
})
|
})
|
||||||
return [parsedItems, configs]
|
return [parsedItems, configs]
|
||||||
@ -186,30 +228,56 @@ export const feedbinServiceHooks: ServiceHooks = {
|
|||||||
const predicates: lf.Predicate[] = [
|
const predicates: lf.Predicate[] = [
|
||||||
db.items.source.in(sids),
|
db.items.source.in(sids),
|
||||||
db.items.hasRead.eq(false),
|
db.items.hasRead.eq(false),
|
||||||
db.items.serviceRef.isNotNull()
|
db.items.serviceRef.isNotNull(),
|
||||||
]
|
]
|
||||||
if (date) {
|
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 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"]))
|
const refs = rows.map(row => parseInt(row["serviceRef"]))
|
||||||
markItems(configs, "unread", "DELETE", refs)
|
markItems(configs, "unread", "DELETE", refs)
|
||||||
},
|
},
|
||||||
|
|
||||||
markRead: (item: RSSItem) => async (_, getState) => {
|
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) => {
|
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) => {
|
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) => {
|
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
|
useInt32?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchAPI(configs: FeverConfigs, params="", postparams="") {
|
async function fetchAPI(configs: FeverConfigs, params = "", postparams = "") {
|
||||||
const response = await fetch(configs.endpoint + "?api" + params, {
|
const response = await fetch(configs.endpoint + "?api" + params, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "content-type": "application/x-www-form-urlencoded" },
|
headers: { "content-type": "application/x-www-form-urlencoded" },
|
||||||
body: `api_key=${configs.apiKey}${postparams}`
|
body: `api_key=${configs.apiKey}${postparams}`,
|
||||||
})
|
})
|
||||||
return await response.json()
|
return await response.json()
|
||||||
}
|
}
|
||||||
@ -29,14 +29,18 @@ async function fetchAPI(configs: FeverConfigs, params="", postparams="") {
|
|||||||
async function markItem(configs: FeverConfigs, item: RSSItem, as: string) {
|
async function markItem(configs: FeverConfigs, item: RSSItem, as: string) {
|
||||||
if (item.serviceRef) {
|
if (item.serviceRef) {
|
||||||
try {
|
try {
|
||||||
await fetchAPI(configs, "", `&mark=item&as=${as}&id=${item.serviceRef}`)
|
await fetchAPI(
|
||||||
|
configs,
|
||||||
|
"",
|
||||||
|
`&mark=item&as=${as}&id=${item.serviceRef}`
|
||||||
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const APIError = () => new Error(intl.get("service.failure"))
|
const APIError = () => new Error(intl.get("service.failure"))
|
||||||
|
|
||||||
export const feverServiceHooks: ServiceHooks = {
|
export const feverServiceHooks: ServiceHooks = {
|
||||||
authenticate: async (configs: FeverConfigs) => {
|
authenticate: async (configs: FeverConfigs) => {
|
||||||
@ -57,7 +61,8 @@ export const feverServiceHooks: ServiceHooks = {
|
|||||||
if (configs.importGroups) {
|
if (configs.importGroups) {
|
||||||
// Import groups on the first sync
|
// Import groups on the first sync
|
||||||
const groups: any[] = (await fetchAPI(configs, "&groups")).groups
|
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>()
|
const groupsIdMap = new Map<number, string>()
|
||||||
for (let group of groups) {
|
for (let group of groups) {
|
||||||
const title = group.title.trim()
|
const title = group.title.trim()
|
||||||
@ -90,7 +95,10 @@ export const feverServiceHooks: ServiceHooks = {
|
|||||||
response = await fetchAPI(configs, `&items&max_id=${min}`)
|
response = await fetchAPI(configs, `&items&max_id=${min}`)
|
||||||
if (response.items === undefined) throw APIError()
|
if (response.items === undefined) throw APIError()
|
||||||
items.push(...response.items.filter(i => i.id > configs.lastId))
|
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
|
configs.useInt32 = true
|
||||||
min = 2147483647
|
min = 2147483647
|
||||||
response = undefined
|
response = undefined
|
||||||
@ -98,11 +106,14 @@ export const feverServiceHooks: ServiceHooks = {
|
|||||||
min = response.items.reduce((m, n) => Math.min(m, n.id), min)
|
min = response.items.reduce((m, n) => Math.min(m, n.id), min)
|
||||||
}
|
}
|
||||||
} while (
|
} while (
|
||||||
min > configs.lastId &&
|
min > configs.lastId &&
|
||||||
(response === undefined || response.items.length >= 50) &&
|
(response === undefined || response.items.length >= 50) &&
|
||||||
items.length < configs.fetchLimit
|
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) {
|
if (items.length > 0) {
|
||||||
const fidMap = new Map<string, RSSSource>()
|
const fidMap = new Map<string, RSSSource>()
|
||||||
for (let source of Object.values(state.sources)) {
|
for (let source of Object.values(state.sources)) {
|
||||||
@ -129,27 +140,33 @@ export const feverServiceHooks: ServiceHooks = {
|
|||||||
} as RSSItem
|
} as RSSItem
|
||||||
// Try to get the thumbnail of the item
|
// Try to get the thumbnail of the item
|
||||||
let dom = domParser.parseFromString(item.content, "text/html")
|
let dom = domParser.parseFromString(item.content, "text/html")
|
||||||
let baseEl = dom.createElement('base')
|
let baseEl = dom.createElement("base")
|
||||||
baseEl.setAttribute('href', item.link.split("/").slice(0, 3).join("/"))
|
baseEl.setAttribute(
|
||||||
|
"href",
|
||||||
|
item.link.split("/").slice(0, 3).join("/")
|
||||||
|
)
|
||||||
dom.head.append(baseEl)
|
dom.head.append(baseEl)
|
||||||
let img = dom.querySelector("img")
|
let img = dom.querySelector("img")
|
||||||
if (img && img.src) {
|
if (img && img.src) {
|
||||||
item.thumb = img.src
|
item.thumb = img.src
|
||||||
} else if (configs.useInt32) { // TTRSS Fever Plugin attachments
|
} else if (configs.useInt32) {
|
||||||
let a = dom.querySelector("body>ul>li:first-child>a") as HTMLAnchorElement
|
// 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)
|
if (a && /, image\/generic$/.test(a.innerText) && a.href)
|
||||||
item.thumb = a.href
|
item.thumb = a.href
|
||||||
}
|
}
|
||||||
// Apply rules and sync back to the service
|
// Apply rules and sync back to the service
|
||||||
if (source.rules) SourceRule.applyAll(source.rules, item)
|
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")
|
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")
|
markItem(configs, item, item.starred ? "saved" : "unsaved")
|
||||||
return item
|
return item
|
||||||
})
|
})
|
||||||
return [parsedItems, configs]
|
return [parsedItems, configs]
|
||||||
} else {
|
} else {
|
||||||
return [[], configs]
|
return [[], configs]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -157,10 +174,13 @@ export const feverServiceHooks: ServiceHooks = {
|
|||||||
syncItems: () => async (_, getState) => {
|
syncItems: () => async (_, getState) => {
|
||||||
const configs = getState().service as FeverConfigs
|
const configs = getState().service as FeverConfigs
|
||||||
const [unreadResponse, starredResponse] = await Promise.all([
|
const [unreadResponse, starredResponse] = await Promise.all([
|
||||||
fetchAPI(configs, "&unread_item_ids"),
|
fetchAPI(configs, "&unread_item_ids"),
|
||||||
fetchAPI(configs, "&saved_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()
|
throw APIError()
|
||||||
}
|
}
|
||||||
const unreadFids: string[] = unreadResponse.unread_item_ids.split(",")
|
const unreadFids: string[] = unreadResponse.unread_item_ids.split(",")
|
||||||
@ -173,7 +193,9 @@ export const feverServiceHooks: ServiceHooks = {
|
|||||||
const configs = state.service as FeverConfigs
|
const configs = state.service as FeverConfigs
|
||||||
if (date && !before) {
|
if (date && !before) {
|
||||||
const iids = state.feeds[state.page.feedId].iids
|
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) {
|
for (let item of items) {
|
||||||
if (item.serviceRef) {
|
if (item.serviceRef) {
|
||||||
markItem(configs, item, "read")
|
markItem(configs, item, "read")
|
||||||
@ -181,10 +203,15 @@ export const feverServiceHooks: ServiceHooks = {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const sources = sids.map(sid => state.sources[sid])
|
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) {
|
for (let source of sources) {
|
||||||
if (source.serviceRef) {
|
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) => {
|
unstar: (item: RSSItem) => async (_, getState) => {
|
||||||
await markItem(getState().service as FeverConfigs, item, "unsaved")
|
await markItem(getState().service as FeverConfigs, item, "unsaved")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,12 @@ export interface GReaderConfigs extends ServiceConfigs {
|
|||||||
removeInoreaderAd?: boolean
|
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()
|
const headers = new Headers()
|
||||||
if (configs.auth !== null) headers.set("Authorization", configs.auth)
|
if (configs.auth !== null) headers.set("Authorization", configs.auth)
|
||||||
if (configs.type == SyncService.Inoreader) {
|
if (configs.type == SyncService.Inoreader) {
|
||||||
@ -40,14 +45,17 @@ async function fetchAPI(configs: GReaderConfigs, params: string, method="GET", b
|
|||||||
headers.set("AppKey", "KPbKYXTfgrKbwmroOeYC7mcW21ZRwF5Y")
|
headers.set("AppKey", "KPbKYXTfgrKbwmroOeYC7mcW21ZRwF5Y")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return await fetch(configs.endpoint + params, {
|
return await fetch(configs.endpoint + params, {
|
||||||
method: method,
|
method: method,
|
||||||
headers: headers,
|
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 results = new Array()
|
||||||
let fetched: any[]
|
let fetched: any[]
|
||||||
let continuation: string
|
let continuation: string
|
||||||
@ -67,8 +75,13 @@ async function fetchAll(configs: GReaderConfigs, params: string): Promise<Set<st
|
|||||||
return new Set(results)
|
return new Set(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function editTag(configs: GReaderConfigs, ref: string, tag: string, add=true) {
|
async function editTag(
|
||||||
const body = new URLSearchParams(`i=${ref}&${add?"a":"r"}=${tag}`)
|
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)
|
return await fetchAPI(configs, "/reader/api/0/edit-tag", "POST", body)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,7 +99,10 @@ export const gReaderServiceHooks: ServiceHooks = {
|
|||||||
authenticate: async (configs: GReaderConfigs) => {
|
authenticate: async (configs: GReaderConfigs) => {
|
||||||
if (configs.auth !== null) {
|
if (configs.auth !== null) {
|
||||||
try {
|
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
|
return result.status === 200
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
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()
|
const body = new URLSearchParams()
|
||||||
body.append("Email", configs.username)
|
body.append("Email", configs.username)
|
||||||
body.append("Passwd", configs.password)
|
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) {
|
if (result.status === 200) {
|
||||||
const text = await result.text()
|
const text = await result.text()
|
||||||
const matches = text.match(/Auth=(\S+)/)
|
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
|
return configs
|
||||||
} else {
|
} else {
|
||||||
throw APIError()
|
throw APIError()
|
||||||
@ -111,7 +135,10 @@ export const gReaderServiceHooks: ServiceHooks = {
|
|||||||
|
|
||||||
updateSources: () => async (dispatch, getState) => {
|
updateSources: () => async (dispatch, getState) => {
|
||||||
const configs = getState().service as GReaderConfigs
|
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()
|
if (response.status !== 200) throw APIError()
|
||||||
const subscriptions: any[] = (await response.json()).subscriptions
|
const subscriptions: any[] = (await response.json()).subscriptions
|
||||||
let groupsMap: Map<string, string>
|
let groupsMap: Map<string, string>
|
||||||
@ -134,7 +161,10 @@ export const gReaderServiceHooks: ServiceHooks = {
|
|||||||
const source = new RSSSource(s.url || s.htmlUrl, s.title)
|
const source = new RSSSource(s.url || s.htmlUrl, s.title)
|
||||||
source.serviceRef = s.id
|
source.serviceRef = s.id
|
||||||
// Omit duplicate sources in The Old Reader
|
// 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)
|
sources.push(source)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -145,13 +175,25 @@ export const gReaderServiceHooks: ServiceHooks = {
|
|||||||
const configs = getState().service as GReaderConfigs
|
const configs = getState().service as GReaderConfigs
|
||||||
if (configs.type == SyncService.Inoreader) {
|
if (configs.type == SyncService.Inoreader) {
|
||||||
return await Promise.all([
|
return await Promise.all([
|
||||||
fetchAll(configs, `/reader/api/0/stream/items/ids?output=json&xt=${READ_TAG}&n=1000`),
|
fetchAll(
|
||||||
fetchAll(configs, `/reader/api/0/stream/items/ids?output=json&it=${STAR_TAG}&n=1000`)
|
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 {
|
} else {
|
||||||
return await Promise.all([
|
return await Promise.all([
|
||||||
fetchAll(configs, `/reader/api/0/stream/items/ids?output=json&s=${ALL_TAG}&xt=${READ_TAG}&n=1000`),
|
fetchAll(
|
||||||
fetchAll(configs, `/reader/api/0/stream/items/ids?output=json&s=${STAR_TAG}&n=1000`)
|
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
|
fetchedItems = fetched.items
|
||||||
for (let i of fetchedItems) {
|
for (let i of fetchedItems) {
|
||||||
i.id = compactId(i.id, configs.useInt64)
|
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
|
break
|
||||||
} else {
|
} else {
|
||||||
items.push(i)
|
items.push(i)
|
||||||
@ -196,9 +241,19 @@ export const gReaderServiceHooks: ServiceHooks = {
|
|||||||
items.map(i => {
|
items.map(i => {
|
||||||
const source = fidMap.get(i.origin.streamId)
|
const source = fidMap.get(i.origin.streamId)
|
||||||
if (source === undefined) return
|
if (source === undefined) return
|
||||||
const dom = domParser.parseFromString(i.summary.content, "text/html")
|
const dom = domParser.parseFromString(
|
||||||
if (configs.type == SyncService.Inoreader && configs.removeInoreaderAd !== false) {
|
i.summary.content,
|
||||||
if (dom.documentElement.textContent.trim().startsWith("Ads from Inoreader")) {
|
"text/html"
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
configs.type == SyncService.Inoreader &&
|
||||||
|
configs.removeInoreaderAd !== false
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
dom.documentElement.textContent
|
||||||
|
.trim()
|
||||||
|
.startsWith("Ads from Inoreader")
|
||||||
|
) {
|
||||||
dom.body.firstChild.remove()
|
dom.body.firstChild.remove()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -215,17 +270,26 @@ export const gReaderServiceHooks: ServiceHooks = {
|
|||||||
starred: false,
|
starred: false,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
notify: false,
|
notify: false,
|
||||||
serviceRef: i.id
|
serviceRef: i.id,
|
||||||
} as RSSItem
|
} as RSSItem
|
||||||
const baseEl = dom.createElement('base')
|
const baseEl = dom.createElement("base")
|
||||||
baseEl.setAttribute('href', item.link.split("/").slice(0, 3).join("/"))
|
baseEl.setAttribute(
|
||||||
|
"href",
|
||||||
|
item.link.split("/").slice(0, 3).join("/")
|
||||||
|
)
|
||||||
dom.head.append(baseEl)
|
dom.head.append(baseEl)
|
||||||
let img = dom.querySelector("img")
|
let img = dom.querySelector("img")
|
||||||
if (img && img.src) item.thumb = img.src
|
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) {
|
for (let c of i.categories) {
|
||||||
if (!item.hasRead && c.endsWith("/state/com.google/read")) item.hasRead = true
|
if (!item.hasRead && c.endsWith("/state/com.google/read"))
|
||||||
else if (!item.starred && c.endsWith("/state/com.google/starred")) item.starred = true
|
item.hasRead = true
|
||||||
|
else if (
|
||||||
|
!item.starred &&
|
||||||
|
c.endsWith("/state/com.google/starred")
|
||||||
|
)
|
||||||
|
item.starred = true
|
||||||
}
|
}
|
||||||
// Apply rules and sync back to the service
|
// Apply rules and sync back to the service
|
||||||
if (source.rules) {
|
if (source.rules) {
|
||||||
@ -233,14 +297,26 @@ export const gReaderServiceHooks: ServiceHooks = {
|
|||||||
const starred = item.starred
|
const starred = item.starred
|
||||||
SourceRule.applyAll(source.rules, item)
|
SourceRule.applyAll(source.rules, item)
|
||||||
if (item.hasRead !== hasRead)
|
if (item.hasRead !== hasRead)
|
||||||
editTag(configs, item.serviceRef, READ_TAG, item.hasRead)
|
editTag(
|
||||||
|
configs,
|
||||||
|
item.serviceRef,
|
||||||
|
READ_TAG,
|
||||||
|
item.hasRead
|
||||||
|
)
|
||||||
if (item.starred !== starred)
|
if (item.starred !== starred)
|
||||||
editTag(configs, item.serviceRef, STAR_TAG, item.starred)
|
editTag(
|
||||||
}
|
configs,
|
||||||
|
item.serviceRef,
|
||||||
|
STAR_TAG,
|
||||||
|
item.starred
|
||||||
|
)
|
||||||
|
}
|
||||||
parsedItems.push(item)
|
parsedItems.push(item)
|
||||||
})
|
})
|
||||||
if (parsedItems.length > 0) {
|
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]
|
return [parsedItems, configs]
|
||||||
} else {
|
} else {
|
||||||
@ -255,13 +331,19 @@ export const gReaderServiceHooks: ServiceHooks = {
|
|||||||
const predicates: lf.Predicate[] = [
|
const predicates: lf.Predicate[] = [
|
||||||
db.items.source.in(sids),
|
db.items.source.in(sids),
|
||||||
db.items.hasRead.eq(false),
|
db.items.hasRead.eq(false),
|
||||||
db.items.serviceRef.isNotNull()
|
db.items.serviceRef.isNotNull(),
|
||||||
]
|
]
|
||||||
if (date) {
|
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 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=")
|
const refs = rows.map(row => row["serviceRef"]).join("&i=")
|
||||||
if (refs) {
|
if (refs) {
|
||||||
editTag(getState().service as GReaderConfigs, refs, READ_TAG)
|
editTag(getState().service as GReaderConfigs, refs, READ_TAG)
|
||||||
@ -272,25 +354,48 @@ export const gReaderServiceHooks: ServiceHooks = {
|
|||||||
if (source.serviceRef) {
|
if (source.serviceRef) {
|
||||||
const body = new URLSearchParams()
|
const body = new URLSearchParams()
|
||||||
body.set("s", source.serviceRef)
|
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) => {
|
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) => {
|
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) => {
|
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) => {
|
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 * as db from "../db"
|
||||||
import lf from "lovefield"
|
import lf from "lovefield"
|
||||||
import { fetchFavicon, ActionStatus, AppThunk, parseRSS } from "../utils"
|
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 { saveSettings } from "./app"
|
||||||
import { SourceRule } from "./rule"
|
import { SourceRule } from "./rule"
|
||||||
import { fixBrokenGroups } from "./group"
|
import { fixBrokenGroups } from "./group"
|
||||||
|
|
||||||
export const enum SourceOpenTarget {
|
export const enum SourceOpenTarget {
|
||||||
Local, Webpage, External, FullContent
|
Local,
|
||||||
|
Webpage,
|
||||||
|
External,
|
||||||
|
FullContent,
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RSSSource {
|
export class RSSSource {
|
||||||
@ -41,13 +52,23 @@ export class RSSSource {
|
|||||||
return feed
|
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)
|
let i = new RSSItem(item, source)
|
||||||
const items = (await db.itemsDB.select().from(db.items).where(lf.op.and(
|
const items = (await db.itemsDB
|
||||||
db.items.source.eq(i.source),
|
.select()
|
||||||
db.items.title.eq(i.title),
|
.from(db.items)
|
||||||
db.items.date.eq(i.date)
|
.where(
|
||||||
)).limit(1).exec()) as RSSItem[]
|
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) {
|
if (items.length === 0) {
|
||||||
RSSItem.parseContent(i, item)
|
RSSItem.parseContent(i, item)
|
||||||
if (source.rules) SourceRule.applyAll(source.rules, i)
|
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) => {
|
return new Promise<RSSItem[]>((resolve, reject) => {
|
||||||
let p = new Array<Promise<RSSItem>>()
|
let p = new Array<Promise<RSSItem>>()
|
||||||
for (let item of items) {
|
for (let item of items) {
|
||||||
p.push(this.checkItem(source, item))
|
p.push(this.checkItem(source, item))
|
||||||
}
|
}
|
||||||
Promise.all(p).then(values => {
|
Promise.all(p)
|
||||||
resolve(values.filter(v => v != null))
|
.then(values => {
|
||||||
}).catch(e => { reject(e) })
|
resolve(values.filter(v => v != null))
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
reject(e)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,17 +139,21 @@ interface UpdateUnreadCountsAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface DeleteSourceAction {
|
interface DeleteSourceAction {
|
||||||
type: typeof DELETE_SOURCE,
|
type: typeof DELETE_SOURCE
|
||||||
source: RSSSource
|
source: RSSSource
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SourceActionTypes = InitSourcesAction | AddSourceAction | UpdateSourceAction
|
export type SourceActionTypes =
|
||||||
| UpdateUnreadCountsAction | DeleteSourceAction
|
| InitSourcesAction
|
||||||
|
| AddSourceAction
|
||||||
|
| UpdateSourceAction
|
||||||
|
| UpdateUnreadCountsAction
|
||||||
|
| DeleteSourceAction
|
||||||
|
|
||||||
export function initSourcesRequest(): SourceActionTypes {
|
export function initSourcesRequest(): SourceActionTypes {
|
||||||
return {
|
return {
|
||||||
type: INIT_SOURCES,
|
type: INIT_SOURCES,
|
||||||
status: ActionStatus.Request
|
status: ActionStatus.Request,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,7 +161,7 @@ export function initSourcesSuccess(sources: SourceState): SourceActionTypes {
|
|||||||
return {
|
return {
|
||||||
type: INIT_SOURCES,
|
type: INIT_SOURCES,
|
||||||
status: ActionStatus.Success,
|
status: ActionStatus.Success,
|
||||||
sources: sources
|
sources: sources,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,19 +169,17 @@ export function initSourcesFailure(err): SourceActionTypes {
|
|||||||
return {
|
return {
|
||||||
type: INIT_SOURCES,
|
type: INIT_SOURCES,
|
||||||
status: ActionStatus.Failure,
|
status: ActionStatus.Failure,
|
||||||
err: err
|
err: err,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function unreadCount(sources: SourceState): Promise<SourceState> {
|
async function unreadCount(sources: SourceState): Promise<SourceState> {
|
||||||
const rows = await db.itemsDB.select(
|
const rows = await db.itemsDB
|
||||||
db.items.source,
|
.select(db.items.source, lf.fn.count(db.items._id))
|
||||||
lf.fn.count(db.items._id)
|
.from(db.items)
|
||||||
).from(db.items).where(
|
.where(db.items.hasRead.eq(false))
|
||||||
db.items.hasRead.eq(false)
|
.groupBy(db.items.source)
|
||||||
).groupBy(
|
.exec()
|
||||||
db.items.source
|
|
||||||
).exec()
|
|
||||||
for (let row of rows) {
|
for (let row of rows) {
|
||||||
sources[row["source"]].unreadCount = row["COUNT(_id)"]
|
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)) {
|
for (let source of Object.values(getState().sources)) {
|
||||||
sources[source.sid] = {
|
sources[source.sid] = {
|
||||||
...source,
|
...source,
|
||||||
unreadCount: 0
|
unreadCount: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dispatch({
|
dispatch({
|
||||||
@ -173,10 +203,13 @@ export function updateUnreadCounts(): AppThunk<Promise<void>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function initSources(): AppThunk<Promise<void>> {
|
export function initSources(): AppThunk<Promise<void>> {
|
||||||
return async (dispatch) => {
|
return async dispatch => {
|
||||||
dispatch(initSourcesRequest())
|
dispatch(initSourcesRequest())
|
||||||
await db.init()
|
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 = {}
|
const state: SourceState = {}
|
||||||
for (let source of sources) {
|
for (let source of sources) {
|
||||||
source.unreadCount = 0
|
source.unreadCount = 0
|
||||||
@ -192,16 +225,19 @@ export function addSourceRequest(batch: boolean): SourceActionTypes {
|
|||||||
return {
|
return {
|
||||||
type: ADD_SOURCE,
|
type: ADD_SOURCE,
|
||||||
batch: batch,
|
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 {
|
return {
|
||||||
type: ADD_SOURCE,
|
type: ADD_SOURCE,
|
||||||
batch: batch,
|
batch: batch,
|
||||||
status: ActionStatus.Success,
|
status: ActionStatus.Success,
|
||||||
source: source
|
source: source,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,7 +246,7 @@ export function addSourceFailure(err, batch: boolean): SourceActionTypes {
|
|||||||
type: ADD_SOURCE,
|
type: ADD_SOURCE,
|
||||||
batch: batch,
|
batch: batch,
|
||||||
status: ActionStatus.Failure,
|
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
|
source.sid = Math.max(...sids, -1) + 1
|
||||||
const row = db.sources.createRow(source)
|
const row = db.sources.createRow(source)
|
||||||
try {
|
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])
|
resolve(inserted[0])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.code === 201) reject(intl.get("sources.exist"))
|
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) => {
|
return async (dispatch, getState) => {
|
||||||
const app = getState().app
|
const app = getState().app
|
||||||
if (app.sourceInit) {
|
if (app.sourceInit) {
|
||||||
@ -253,7 +297,10 @@ export function addSource(url: string, name: string = null, batch = false): AppT
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch(addSourceFailure(e, batch))
|
dispatch(addSourceFailure(e, batch))
|
||||||
if (!batch) {
|
if (!batch) {
|
||||||
window.utils.showErrorBox(intl.get("sources.errorAdd"), String(e))
|
window.utils.showErrorBox(
|
||||||
|
intl.get("sources.errorAdd"),
|
||||||
|
String(e)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
@ -265,16 +312,20 @@ export function addSource(url: string, name: string = null, batch = false): AppT
|
|||||||
export function updateSourceDone(source: RSSSource): SourceActionTypes {
|
export function updateSourceDone(source: RSSSource): SourceActionTypes {
|
||||||
return {
|
return {
|
||||||
type: UPDATE_SOURCE,
|
type: UPDATE_SOURCE,
|
||||||
source: source
|
source: source,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateSource(source: RSSSource): AppThunk<Promise<void>> {
|
export function updateSource(source: RSSSource): AppThunk<Promise<void>> {
|
||||||
return async (dispatch) => {
|
return async dispatch => {
|
||||||
let sourceCopy = { ...source }
|
let sourceCopy = { ...source }
|
||||||
delete sourceCopy.unreadCount
|
delete sourceCopy.unreadCount
|
||||||
const row = db.sources.createRow(sourceCopy)
|
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))
|
dispatch(updateSourceDone(source))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -282,16 +333,27 @@ export function updateSource(source: RSSSource): AppThunk<Promise<void>> {
|
|||||||
export function deleteSourceDone(source: RSSSource): SourceActionTypes {
|
export function deleteSourceDone(source: RSSSource): SourceActionTypes {
|
||||||
return {
|
return {
|
||||||
type: DELETE_SOURCE,
|
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) => {
|
return async (dispatch, getState) => {
|
||||||
if (!batch) dispatch(saveSettings())
|
if (!batch) dispatch(saveSettings())
|
||||||
try {
|
try {
|
||||||
await db.itemsDB.delete().from(db.items).where(db.items.source.eq(source.sid)).exec()
|
await db.itemsDB
|
||||||
await db.sourcesDB.delete().from(db.sources).where(db.sources.sid.eq(source.sid)).exec()
|
.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))
|
dispatch(deleteSourceDone(source))
|
||||||
window.settings.saveGroups(getState().groups)
|
window.settings.saveGroups(getState().groups)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -303,7 +365,7 @@ export function deleteSource(source: RSSSource, batch = false): AppThunk<Promise
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function deleteSources(sources: RSSSource[]): AppThunk<Promise<void>> {
|
export function deleteSources(sources: RSSSource[]): AppThunk<Promise<void>> {
|
||||||
return async (dispatch) => {
|
return async dispatch => {
|
||||||
dispatch(saveSettings())
|
dispatch(saveSettings())
|
||||||
for (let source of sources) {
|
for (let source of sources) {
|
||||||
await dispatch(deleteSource(source, true))
|
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) => {
|
return async (dispatch, getState) => {
|
||||||
const initSources = getState().sources
|
const initSources = getState().sources
|
||||||
if (!sids) {
|
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 {
|
} else {
|
||||||
sids = sids.filter(sid => sid in initSources)
|
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
|
const url = initSources[sid].url
|
||||||
let favicon = (await fetchFavicon(url)) || ""
|
let favicon = (await fetchFavicon(url)) || ""
|
||||||
const source = getState().sources[sid]
|
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
|
source.iconurl = favicon
|
||||||
await dispatch(updateSource(source))
|
await dispatch(updateSource(source))
|
||||||
}
|
}
|
||||||
@ -340,22 +411,28 @@ export function sourceReducer(
|
|||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case INIT_SOURCES:
|
case INIT_SOURCES:
|
||||||
switch (action.status) {
|
switch (action.status) {
|
||||||
case ActionStatus.Success: return action.sources
|
case ActionStatus.Success:
|
||||||
default: return state
|
return action.sources
|
||||||
|
default:
|
||||||
|
return state
|
||||||
}
|
}
|
||||||
case UPDATE_UNREAD_COUNTS: return action.sources
|
case UPDATE_UNREAD_COUNTS:
|
||||||
|
return action.sources
|
||||||
case ADD_SOURCE:
|
case ADD_SOURCE:
|
||||||
switch (action.status) {
|
switch (action.status) {
|
||||||
case ActionStatus.Success: return {
|
case ActionStatus.Success:
|
||||||
...state,
|
return {
|
||||||
[action.source.sid]: action.source
|
...state,
|
||||||
}
|
[action.source.sid]: action.source,
|
||||||
default: return state
|
}
|
||||||
|
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: {
|
case DELETE_SOURCE: {
|
||||||
delete state[action.source.sid]
|
delete state[action.source.sid]
|
||||||
return { ...state }
|
return { ...state }
|
||||||
@ -365,10 +442,14 @@ export function sourceReducer(
|
|||||||
case ActionStatus.Success: {
|
case ActionStatus.Success: {
|
||||||
let updateMap = new Map<number, number>()
|
let updateMap = new Map<number, number>()
|
||||||
for (let item of action.items) {
|
for (let item of action.items) {
|
||||||
if (!item.hasRead) { updateMap.set(
|
if (!item.hasRead) {
|
||||||
item.source,
|
updateMap.set(
|
||||||
updateMap.has(item.source) ? (updateMap.get(item.source) + 1) : 1
|
item.source,
|
||||||
)}
|
updateMap.has(item.source)
|
||||||
|
? updateMap.get(item.source) + 1
|
||||||
|
: 1
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let nextState = {} as SourceState
|
let nextState = {} as SourceState
|
||||||
for (let [s, source] of Object.entries(state)) {
|
for (let [s, source] of Object.entries(state)) {
|
||||||
@ -376,7 +457,8 @@ export function sourceReducer(
|
|||||||
if (updateMap.has(sid)) {
|
if (updateMap.has(sid)) {
|
||||||
nextState[sid] = {
|
nextState[sid] = {
|
||||||
...source,
|
...source,
|
||||||
unreadCount: source.unreadCount + updateMap.get(sid)
|
unreadCount:
|
||||||
|
source.unreadCount + updateMap.get(sid),
|
||||||
} as RSSSource
|
} as RSSSource
|
||||||
} else {
|
} else {
|
||||||
nextState[sid] = source
|
nextState[sid] = source
|
||||||
@ -384,27 +466,32 @@ export function sourceReducer(
|
|||||||
}
|
}
|
||||||
return nextState
|
return nextState
|
||||||
}
|
}
|
||||||
default: return state
|
default:
|
||||||
|
return state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case MARK_UNREAD:
|
case MARK_UNREAD:
|
||||||
case MARK_READ: return {
|
case MARK_READ:
|
||||||
...state,
|
return {
|
||||||
[action.item.source]: {
|
...state,
|
||||||
...state[action.item.source],
|
[action.item.source]: {
|
||||||
unreadCount: state[action.item.source].unreadCount + (action.type === MARK_UNREAD ? 1 : -1)
|
...state[action.item.source],
|
||||||
} as RSSSource
|
unreadCount:
|
||||||
}
|
state[action.item.source].unreadCount +
|
||||||
|
(action.type === MARK_UNREAD ? 1 : -1),
|
||||||
|
} as RSSSource,
|
||||||
|
}
|
||||||
case MARK_ALL_READ: {
|
case MARK_ALL_READ: {
|
||||||
let nextState = { ...state }
|
let nextState = { ...state }
|
||||||
action.sids.map((sid, i) => {
|
action.sids.map((sid, i) => {
|
||||||
nextState[sid] = {
|
nextState[sid] = {
|
||||||
...state[sid],
|
...state[sid],
|
||||||
unreadCount: action.time ? state[sid].unreadCount : 0
|
unreadCount: action.time ? state[sid].unreadCount : 0,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return nextState
|
return nextState
|
||||||
}
|
}
|
||||||
default: return state
|
default:
|
||||||
|
return state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ export const rootReducer = combineReducers({
|
|||||||
groups: groupReducer,
|
groups: groupReducer,
|
||||||
page: pageReducer,
|
page: pageReducer,
|
||||||
service: serviceReducer,
|
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"
|
import intl from "react-intl-universal"
|
||||||
|
|
||||||
const lightTheme: IPartialTheme = {
|
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 = {
|
const darkTheme: IPartialTheme = {
|
||||||
...lightTheme,
|
...lightTheme,
|
||||||
@ -33,8 +36,8 @@ const darkTheme: IPartialTheme = {
|
|||||||
themeDarkAlt: "#4ba0e1",
|
themeDarkAlt: "#4ba0e1",
|
||||||
themeDark: "#65aee6",
|
themeDark: "#65aee6",
|
||||||
themeDarker: "#8ac2ec",
|
themeDarker: "#8ac2ec",
|
||||||
accent: "#3a96dd"
|
accent: "#3a96dd",
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setThemeSettings(theme: ThemeSettings) {
|
export function setThemeSettings(theme: ThemeSettings) {
|
||||||
@ -47,7 +50,7 @@ export function getThemeSettings(): ThemeSettings {
|
|||||||
export function applyThemeSettings() {
|
export function applyThemeSettings() {
|
||||||
loadTheme(window.settings.shouldUseDarkColors() ? darkTheme : lightTheme)
|
loadTheme(window.settings.shouldUseDarkColors() ? darkTheme : lightTheme)
|
||||||
}
|
}
|
||||||
window.settings.addThemeUpdateListener((shouldDark) => {
|
window.settings.addThemeUpdateListener(shouldDark => {
|
||||||
loadTheme(shouldDark ? darkTheme : lightTheme)
|
loadTheme(shouldDark ? darkTheme : lightTheme)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -55,12 +58,15 @@ export function getCurrentLocale() {
|
|||||||
let locale = window.settings.getCurrentLocale()
|
let locale = window.settings.getCurrentLocale()
|
||||||
if (locale in locales) return locale
|
if (locale in locales) return locale
|
||||||
locale = locale.split("-")[0]
|
locale = locale.split("-")[0]
|
||||||
return (locale in locales) ? locale : "en-US"
|
return locale in locales ? locale : "en-US"
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function exportAll() {
|
export async function exportAll() {
|
||||||
const filters = [{ name: intl.get("app.frData"), extensions: ["frdata"] }]
|
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) {
|
if (write) {
|
||||||
let output = window.settings.getAll()
|
let output = window.settings.getAll()
|
||||||
output["lovefield"] = {
|
output["lovefield"] = {
|
||||||
@ -78,8 +84,10 @@ export async function importAll() {
|
|||||||
let confirmed = await window.utils.showMessageBox(
|
let confirmed = await window.utils.showMessageBox(
|
||||||
intl.get("app.restore"),
|
intl.get("app.restore"),
|
||||||
intl.get("app.confirmImport"),
|
intl.get("app.confirmImport"),
|
||||||
intl.get("confirm"), intl.get("cancel"),
|
intl.get("confirm"),
|
||||||
true, "warning"
|
intl.get("cancel"),
|
||||||
|
true,
|
||||||
|
"warning"
|
||||||
)
|
)
|
||||||
if (!confirmed) return true
|
if (!confirmed) return true
|
||||||
let configs = JSON.parse(data)
|
let configs = JSON.parse(data)
|
||||||
@ -90,14 +98,19 @@ export async function importAll() {
|
|||||||
configs.useNeDB = true
|
configs.useNeDB = true
|
||||||
openRequest.onsuccess = () => {
|
openRequest.onsuccess = () => {
|
||||||
let db = openRequest.result
|
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]) => {
|
let requests = Object.entries(configs.nedb).map(([key, value]) => {
|
||||||
return objectStore.put(value, key)
|
return objectStore.put(value, key)
|
||||||
})
|
})
|
||||||
let promises = requests.map(req => new Promise<void>((resolve, reject) => {
|
let promises = requests.map(
|
||||||
req.onsuccess = () => resolve()
|
req =>
|
||||||
req.onerror = () => reject()
|
new Promise<void>((resolve, reject) => {
|
||||||
}))
|
req.onsuccess = () => resolve()
|
||||||
|
req.onerror = () => reject()
|
||||||
|
})
|
||||||
|
)
|
||||||
Promise.all(promises).then(() => {
|
Promise.all(promises).then(() => {
|
||||||
delete configs.nedb
|
delete configs.nedb
|
||||||
window.settings.setAll(configs)
|
window.settings.setAll(configs)
|
||||||
@ -117,6 +130,6 @@ export async function importAll() {
|
|||||||
await db.itemsDB.insert().into(db.items).values(iRows).exec()
|
await db.itemsDB.insert().into(db.items).values(iRows).exec()
|
||||||
delete configs.lovefield
|
delete configs.lovefield
|
||||||
window.settings.setAll(configs)
|
window.settings.setAll(configs)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -7,14 +7,17 @@ import Url from "url"
|
|||||||
import { SearchEngines } from "../schema-types"
|
import { SearchEngines } from "../schema-types"
|
||||||
|
|
||||||
export enum ActionStatus {
|
export enum ActionStatus {
|
||||||
Request, Success, Failure, Intermediate
|
Request,
|
||||||
|
Success,
|
||||||
|
Failure,
|
||||||
|
Intermediate,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AppThunk<ReturnType = void> = ThunkAction<
|
export type AppThunk<ReturnType = void> = ThunkAction<
|
||||||
ReturnType,
|
ReturnType,
|
||||||
RootState,
|
RootState,
|
||||||
unknown,
|
unknown,
|
||||||
AnyAction
|
AnyAction
|
||||||
>
|
>
|
||||||
|
|
||||||
export type AppDispatch = ThunkDispatch<RootState, undefined, AnyAction>
|
export type AppDispatch = ThunkDispatch<RootState, undefined, AnyAction>
|
||||||
@ -22,32 +25,47 @@ export type AppDispatch = ThunkDispatch<RootState, undefined, AnyAction>
|
|||||||
const rssParser = new Parser({
|
const rssParser = new Parser({
|
||||||
customFields: {
|
customFields: {
|
||||||
item: [
|
item: [
|
||||||
"thumb", "image", ["content:encoded", "fullContent"],
|
"thumb",
|
||||||
['media:content', 'mediaContent', {keepArray: true}],
|
"image",
|
||||||
] as Parser.CustomFieldItem[]
|
["content:encoded", "fullContent"],
|
||||||
}
|
["media:content", "mediaContent", { keepArray: true }],
|
||||||
|
] as Parser.CustomFieldItem[],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const CHARSET_RE = /charset=([^()<>@,;:\"/[\]?.=\s]*)/i
|
const CHARSET_RE = /charset=([^()<>@,;:\"/[\]?.=\s]*)/i
|
||||||
const XML_ENCODING_RE = /^<\?xml.+encoding="(.+?)".*?\?>/i
|
const XML_ENCODING_RE = /^<\?xml.+encoding="(.+?)".*?\?>/i
|
||||||
export async function decodeFetchResponse(response: Response, isHTML = false) {
|
export async function decodeFetchResponse(response: Response, isHTML = false) {
|
||||||
const buffer = await response.arrayBuffer()
|
const buffer = await response.arrayBuffer()
|
||||||
let ctype = response.headers.has("content-type") && response.headers.get("content-type")
|
let ctype =
|
||||||
let charset = (ctype && CHARSET_RE.test(ctype)) ? CHARSET_RE.exec(ctype)[1] : undefined
|
response.headers.has("content-type") &&
|
||||||
let content = (new TextDecoder(charset)).decode(buffer)
|
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 (charset === undefined) {
|
||||||
if (isHTML) {
|
if (isHTML) {
|
||||||
const dom = domParser.parseFromString(content, "text/html")
|
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) {
|
if (!charset) {
|
||||||
ctype = dom.querySelector("meta[http-equiv='Content-Type']")?.getAttribute("content")
|
ctype = dom
|
||||||
charset = ctype && CHARSET_RE.test(ctype) && CHARSET_RE.exec(ctype)[1].toLowerCase()
|
.querySelector("meta[http-equiv='Content-Type']")
|
||||||
|
?.getAttribute("content")
|
||||||
|
charset =
|
||||||
|
ctype &&
|
||||||
|
CHARSET_RE.test(ctype) &&
|
||||||
|
CHARSET_RE.exec(ctype)[1].toLowerCase()
|
||||||
}
|
}
|
||||||
} else {
|
} 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") {
|
if (charset && charset !== "utf-8" && charset !== "utf8") {
|
||||||
content = (new TextDecoder(charset)).decode(buffer)
|
content = new TextDecoder(charset).decode(buffer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return content
|
return content
|
||||||
@ -62,7 +80,9 @@ export async function parseRSS(url: string) {
|
|||||||
}
|
}
|
||||||
if (result && result.ok) {
|
if (result && result.ok) {
|
||||||
try {
|
try {
|
||||||
return await rssParser.parseString(await decodeFetchResponse(result))
|
return await rssParser.parseString(
|
||||||
|
await decodeFetchResponse(result)
|
||||||
|
)
|
||||||
} catch {
|
} catch {
|
||||||
throw new Error(intl.get("log.parseError"))
|
throw new Error(intl.get("log.parseError"))
|
||||||
}
|
}
|
||||||
@ -83,7 +103,10 @@ export async function fetchFavicon(url: string) {
|
|||||||
let links = dom.getElementsByTagName("link")
|
let links = dom.getElementsByTagName("link")
|
||||||
for (let link of links) {
|
for (let link of links) {
|
||||||
let rel = link.getAttribute("rel")
|
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 href = link.getAttribute("href")
|
||||||
let parsedUrl = Url.parse(url)
|
let parsedUrl = Url.parse(url)
|
||||||
if (href.startsWith("//")) return parsedUrl.protocol + href
|
if (href.startsWith("//")) return parsedUrl.protocol + href
|
||||||
@ -93,7 +116,7 @@ export async function fetchFavicon(url: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
url = url + "/favicon.ico"
|
url = url + "/favicon.ico"
|
||||||
if (await validateFavicon(url)) {
|
if (await validateFavicon(url)) {
|
||||||
return url
|
return url
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
@ -107,8 +130,11 @@ export async function validateFavicon(url: string) {
|
|||||||
let flag = false
|
let flag = false
|
||||||
try {
|
try {
|
||||||
const result = await fetch(url, { credentials: "omit" })
|
const result = await fetch(url, { credentials: "omit" })
|
||||||
if (result.status == 200 && result.headers.has("Content-Type")
|
if (
|
||||||
&& result.headers.get("Content-Type").startsWith("image")) {
|
result.status == 200 &&
|
||||||
|
result.headers.has("Content-Type") &&
|
||||||
|
result.headers.get("Content-Type").startsWith("image")
|
||||||
|
) {
|
||||||
flag = true
|
flag = true
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@ -121,41 +147,55 @@ export function htmlDecode(input: string) {
|
|||||||
return doc.documentElement.textContent
|
return doc.documentElement.textContent
|
||||||
}
|
}
|
||||||
|
|
||||||
export const urlTest = (s: string) =>
|
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)
|
/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 getWindowBreakpoint = () => window.outerWidth >= 1440
|
||||||
|
|
||||||
export const cutText = (s: string, length: number) => {
|
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) {
|
export function getSearchEngineName(engine: SearchEngines) {
|
||||||
switch (engine) {
|
switch (engine) {
|
||||||
case SearchEngines.Google:
|
case SearchEngines.Google:
|
||||||
return intl.get("searchEngine.google")
|
return intl.get("searchEngine.google")
|
||||||
case SearchEngines.Bing:
|
case SearchEngines.Bing:
|
||||||
return intl.get("searchEngine.bing")
|
return intl.get("searchEngine.bing")
|
||||||
case SearchEngines.Baidu:
|
case SearchEngines.Baidu:
|
||||||
return intl.get("searchEngine.baidu")
|
return intl.get("searchEngine.baidu")
|
||||||
case SearchEngines.DuckDuckGo:
|
case SearchEngines.DuckDuckGo:
|
||||||
return intl.get("searchEngine.duckduckgo")
|
return intl.get("searchEngine.duckduckgo")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export function webSearch(text: string, engine=SearchEngines.Google) {
|
export function webSearch(text: string, engine = SearchEngines.Google) {
|
||||||
switch (engine) {
|
switch (engine) {
|
||||||
case SearchEngines.Google:
|
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:
|
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:
|
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:
|
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 merged = new Array<T>()
|
||||||
let i = 0
|
let i = 0
|
||||||
let j = 0
|
let j = 0
|
||||||
@ -177,14 +217,14 @@ export function byteToMB(B: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function byteLength(str: string) {
|
function byteLength(str: string) {
|
||||||
var s = str.length;
|
var s = str.length
|
||||||
for (var i = str.length - 1; i >= 0; i--) {
|
for (var i = str.length - 1; i >= 0; i--) {
|
||||||
var code = str.charCodeAt(i);
|
var code = str.charCodeAt(i)
|
||||||
if (code > 0x7f && code <= 0x7ff) s++;
|
if (code > 0x7f && code <= 0x7ff) s++
|
||||||
else if (code > 0x7ff && code <= 0xffff) s += 2;
|
else if (code > 0x7ff && code <= 0xffff) s += 2
|
||||||
if (code >= 0xDC00 && code <= 0xDFFF) i--; //trail surrogate
|
if (code >= 0xdc00 && code <= 0xdfff) i-- //trail surrogate
|
||||||
}
|
}
|
||||||
return s;
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateItemSize(): Promise<number> {
|
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
|
return window.utils.platform === "darwin" ? e.metaKey : e.ctrlKey
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,6 +270,6 @@ export function initTouchBarWithTexts() {
|
|||||||
search: intl.get("search"),
|
search: intl.get("search"),
|
||||||
refresh: intl.get("nav.refresh"),
|
refresh: intl.get("nav.refresh"),
|
||||||
markAll: intl.get("nav.markAllRead"),
|
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 HtmlWebpackPlugin = require("html-webpack-plugin")
|
||||||
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
|
const HardSourceWebpackPlugin = require("hard-source-webpack-plugin")
|
||||||
|
|
||||||
module.exports = [
|
module.exports = [
|
||||||
{
|
{
|
||||||
mode: 'production',
|
mode: "production",
|
||||||
entry: './src/electron.ts',
|
entry: "./src/electron.ts",
|
||||||
target: 'electron-main',
|
target: "electron-main",
|
||||||
module: {
|
module: {
|
||||||
rules: [{
|
rules: [
|
||||||
test: /\.ts$/,
|
{
|
||||||
include: /src/,
|
test: /\.ts$/,
|
||||||
resolve: {
|
include: /src/,
|
||||||
extensions: ['.ts', '.js']
|
resolve: {
|
||||||
|
extensions: [".ts", ".js"],
|
||||||
|
},
|
||||||
|
use: [{ loader: "ts-loader" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
use: [{ loader: 'ts-loader' }]
|
output: {
|
||||||
}]
|
devtoolModuleFilenameTemplate: "[absolute-resource-path]",
|
||||||
},
|
path: __dirname + "/dist",
|
||||||
output: {
|
filename: "electron.js",
|
||||||
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']
|
|
||||||
},
|
},
|
||||||
use: [{ loader: 'ts-loader' }]
|
plugins: [new HardSourceWebpackPlugin()],
|
||||||
}]
|
|
||||||
},
|
},
|
||||||
output: {
|
{
|
||||||
path: __dirname + '/dist',
|
mode: "production",
|
||||||
filename: 'preload.js'
|
entry: "./src/preload.ts",
|
||||||
},
|
target: "electron-preload",
|
||||||
plugins: [
|
module: {
|
||||||
new HardSourceWebpackPlugin()
|
rules: [
|
||||||
]
|
{
|
||||||
},
|
test: /\.ts$/,
|
||||||
{
|
include: /src/,
|
||||||
mode: 'production',
|
resolve: {
|
||||||
entry: './src/index.tsx',
|
extensions: [".ts", ".js"],
|
||||||
target: 'web',
|
},
|
||||||
devtool: 'source-map',
|
use: [{ loader: "ts-loader" }],
|
||||||
performance: {
|
},
|
||||||
hints: false
|
],
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [{
|
|
||||||
test: /\.ts(x?)$/,
|
|
||||||
include: /src/,
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.ts', '.tsx', '.js']
|
|
||||||
},
|
},
|
||||||
use: [{ loader: 'ts-loader' }]
|
output: {
|
||||||
}]
|
path: __dirname + "/dist",
|
||||||
|
filename: "preload.js",
|
||||||
|
},
|
||||||
|
plugins: [new HardSourceWebpackPlugin()],
|
||||||
},
|
},
|
||||||
output: {
|
{
|
||||||
path: __dirname + '/dist',
|
mode: "production",
|
||||||
filename: 'index.js'
|
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…
x
Reference in New Issue
Block a user