Compare commits
513 Commits
v1.1
...
84920cc4cf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84920cc4cf | ||
|
|
e9676491ee | ||
|
|
1a545bb2a1 | ||
|
|
1e128a7cd8 | ||
|
|
5dbb6a710c | ||
|
|
dadadeb066 | ||
|
|
c76ff26bd6 | ||
|
|
50f8648f64 | ||
|
|
5f82a9e339 | ||
|
|
3278ba4eac | ||
|
|
9fc72f8b68 | ||
|
|
b7b707bd43 | ||
|
|
7cf27e0fde | ||
|
|
66f2a973a3 | ||
|
|
7ecbbff18a | ||
|
|
850ce195a0 | ||
|
|
479aebd023 | ||
|
|
9b178d1fb3 | ||
|
|
3ab098db5c | ||
|
|
6d16e93008 | ||
|
|
98934daee4 | ||
|
|
259474cae9 | ||
|
|
1e65a7951b | ||
|
|
bed5640366 | ||
|
|
57ea83cf4f | ||
|
|
219842d723 | ||
|
|
a96fc101f2 | ||
|
|
81a77ce0a4 | ||
|
|
9ed359f964 | ||
|
|
bc18557820 | ||
|
|
7d99edab8d | ||
|
|
32ca121520 | ||
|
|
9f1a0534a3 | ||
|
|
d2678be96d | ||
|
|
95ebbb9d13 | ||
|
|
0f6d4d639d | ||
|
|
795a5d2cb4 | ||
|
|
dd5f760606 | ||
|
|
58d6a46e36 | ||
|
|
a8d7b86cdc | ||
|
|
aac3de7ca2 | ||
|
|
de24659bae | ||
|
|
632412c10e | ||
|
|
012b58bbe4 | ||
|
|
c092842ee4 | ||
|
|
e4c1d01915 | ||
|
|
ce07ddea92 | ||
|
|
bd6322e533 | ||
|
|
91da774286 | ||
|
|
e62906e63d | ||
|
|
56e5625adc | ||
|
|
1ecf4b0bb4 | ||
|
|
57d9421c7f | ||
|
|
a73188944d | ||
|
|
97904cc0f3 | ||
|
|
f28f354992 | ||
|
|
698f5d6d06 | ||
|
|
b935a1c511 | ||
|
|
10e6bfa5a0 | ||
|
|
f030a4075b | ||
|
|
c9dd977600 | ||
|
|
c1bcc0c517 | ||
|
|
2a5692d9a7 | ||
|
|
a8d160f9b1 | ||
|
|
286cbff236 | ||
|
|
fff0870d3b | ||
|
|
fe22460c07 | ||
|
|
18f2789a5d | ||
|
|
7f161a5408 | ||
|
|
cba3fbc48c | ||
|
|
5e46f1480e | ||
|
|
ead253c55f | ||
|
|
6b8da92cb3 | ||
|
|
a91f64ce9d | ||
|
|
e1a6ccf133 | ||
|
|
d2c034a850 | ||
|
|
713930decc | ||
|
|
ee2a825cf0 | ||
|
|
8e9da86f83 | ||
|
|
9eb49fd3a7 | ||
|
|
684bc25b83 | ||
|
|
8ceab03cd7 | ||
|
|
34dad4ac8f | ||
|
|
b40d930f8a | ||
|
|
d4b34e900e | ||
|
|
954b549029 | ||
|
|
fbd0b2310e | ||
|
|
be7af0ccaf | ||
|
|
18221ef12d | ||
|
|
4c0726412b | ||
|
|
d7253a60b8 | ||
|
|
2de3ddff08 | ||
|
|
830248b6ae | ||
|
|
f8db2ef7ad | ||
|
|
109caaa889 | ||
|
|
d0b83babd2 | ||
|
|
de3decbffd | ||
|
|
c92229a698 | ||
|
|
176852b662 | ||
|
|
52cc8ecbbd | ||
|
|
e3e9542f1e | ||
|
|
b78c8bf8bf | ||
|
|
bff7476b58 | ||
|
|
05f5785660 | ||
|
|
cb50aed89a | ||
|
|
df655aca5e | ||
|
|
86853a87bf | ||
|
|
e3109a4384 | ||
|
|
eee8002d69 | ||
|
|
92f11f7513 | ||
|
|
5428e6be3a | ||
|
|
1ad693f931 | ||
|
|
c2d88a7e3f | ||
|
|
3b29d737eb | ||
|
|
fe178b8fc6 | ||
|
|
cca742a1c2 | ||
|
|
c7eddff118 | ||
|
|
cf30ed249f | ||
|
|
26b87dee98 | ||
|
|
77c7f938f1 | ||
|
|
f98de9a0a5 | ||
|
|
6fa2b67024 | ||
|
|
355e5feb62 | ||
|
|
a7dd707062 | ||
|
|
4de46a7bc5 | ||
|
|
2c6fce3322 | ||
|
|
19ecfcd0bc | ||
|
|
d575acfe80 | ||
|
|
d203d38de6 | ||
|
|
9f01f63613 | ||
|
|
982c4ebbbc | ||
|
|
0c5385cef3 | ||
|
|
58f4e1f6c9 | ||
|
|
6b7f69d5c0 | ||
|
|
7aeb458ee5 | ||
|
|
7cfd3b3238 | ||
|
|
55262d38fe | ||
|
|
a45e29feb7 | ||
|
|
9f5fd3bb4d | ||
|
|
63f9d55903 | ||
|
|
8f36ae013e | ||
|
|
851aa1a136 | ||
|
|
f38dcfba3b | ||
|
|
214c7aacfc | ||
|
|
eb9bfc57e2 | ||
|
|
c072783c42 | ||
|
|
9d701678e1 | ||
|
|
37ed856d8b | ||
|
|
28f08ad42a | ||
|
|
da267a56ef | ||
|
|
16e4cad9ad | ||
|
|
d13a04898e | ||
|
|
ff39fbff70 | ||
|
|
92c6aac49e | ||
|
|
4ca81f90e9 | ||
|
|
75e828cb4c | ||
|
|
883214a740 | ||
|
|
36e359c881 | ||
|
|
87b53fb8ec | ||
|
|
2ae62855cc | ||
|
|
19889c1457 | ||
|
|
f9afbac258 | ||
|
|
e54df07a40 | ||
|
|
f8455236dc | ||
|
|
d308bb64c2 | ||
|
|
077715f6c2 | ||
|
|
bfe7bfdbd5 | ||
|
|
1013cd1122 | ||
|
|
3e57ccc999 | ||
|
|
5f23f8be89 | ||
|
|
211b1456c7 | ||
|
|
3d9c9d03cc | ||
|
|
1ea8160f7d | ||
|
|
2c12875199 | ||
|
|
bb0b575eca | ||
|
|
e51ccb723e | ||
|
|
96796702cf | ||
|
|
fd44c98cd0 | ||
|
|
c2a28bcadf | ||
|
|
30e6afb096 | ||
|
|
882be1dbf6 | ||
|
|
8764891b80 | ||
|
|
fbb0dfed47 | ||
|
|
42b36965c5 | ||
|
|
e326c7a0fb | ||
|
|
9fae33f57b | ||
|
|
a397d2013d | ||
|
|
f65aadb055 | ||
|
|
c825f8864f | ||
|
|
2edf11a36a | ||
|
|
2df2f41516 | ||
|
|
614dcc8975 | ||
|
|
6acf9af887 | ||
|
|
ecdfcb5017 | ||
|
|
144fc1606a | ||
|
|
9919d72be0 | ||
|
|
9e95f71de8 | ||
|
|
09bfc47ef0 | ||
|
|
2e5ccc3158 | ||
|
|
70481dff73 | ||
|
|
cc12f27ce3 | ||
|
|
c36d82636d | ||
|
|
b123753d65 | ||
|
|
f590c358d2 | ||
|
|
fa2fad0ff6 | ||
|
|
d8aab7acae | ||
|
|
63ad971890 | ||
|
|
0828d6782e | ||
|
|
cf5856bdf7 | ||
|
|
34edfc0727 | ||
|
|
a1b1686d3b | ||
|
|
c5abf8f9d0 | ||
|
|
9a5af089eb | ||
|
|
9edd865bf4 | ||
|
|
e50c7e1a51 | ||
|
|
8967936fb6 | ||
|
|
fa92ea16b0 | ||
|
|
c8d6363677 | ||
|
|
b082c3e048 | ||
|
|
82fdb3be6c | ||
|
|
d7ba203f28 | ||
|
|
1cba53f7fb | ||
|
|
0a0db68905 | ||
|
|
3512350a22 | ||
|
|
8b2a9d8f20 | ||
|
|
34b50d388a | ||
|
|
cd412a4ac5 | ||
|
|
7fb6271e56 | ||
|
|
2cd815d9cd | ||
|
|
0a6e621c02 | ||
|
|
10c656a3b6 | ||
|
|
0ea313d945 | ||
|
|
1f02bde5e1 | ||
|
|
3e0c784744 | ||
|
|
528df7fb4a | ||
|
|
b04e8c1e93 | ||
|
|
0b8bf50204 | ||
|
|
f43924c17b | ||
|
|
0f519b7202 | ||
|
|
c74eeff790 | ||
|
|
e7b645a68a | ||
|
|
fc0bfd29db | ||
|
|
8950181f21 | ||
|
|
70e592c979 | ||
|
|
401668e413 | ||
|
|
37ddde1765 | ||
|
|
82586dedff | ||
|
|
ac36892150 | ||
|
|
c958ee9116 | ||
|
|
e5920259b6 | ||
|
|
8c44d2fc87 | ||
|
|
332ee0e6b5 | ||
|
|
3ae17171e2 | ||
|
|
493a4262b1 | ||
|
|
485587825c | ||
|
|
a83d43a5b1 | ||
|
|
71f81a3802 | ||
|
|
6481c97645 | ||
|
|
fa40a79d50 | ||
|
|
169d579400 | ||
|
|
430f300140 | ||
|
|
a9b450db03 | ||
|
|
89ce8df0e3 | ||
|
|
aa015b78c0 | ||
|
|
3ed1b3e612 | ||
|
|
7fb0d3833e | ||
|
|
2da616d4ff | ||
|
|
0b3d7faf9f | ||
|
|
b3ba912566 | ||
|
|
36bc84d99a | ||
|
|
f126247262 | ||
|
|
b145b00f8e | ||
|
|
7dbfecdba1 | ||
|
|
ad693aaf02 | ||
|
|
fafa6286d4 | ||
|
|
cc51fe01c2 | ||
|
|
91deb41d5b | ||
|
|
2e4082df77 | ||
|
|
51cbdea31f | ||
|
|
5335863488 | ||
|
|
c2e1926741 | ||
|
|
37a679fc80 | ||
|
|
1be79d922b | ||
|
|
6685bce51c | ||
|
|
fe1a1987bd | ||
|
|
80402943a1 | ||
|
|
b40fe94147 | ||
|
|
e0e6166cdf | ||
|
|
a2bfd1682b | ||
|
|
1f393faf79 | ||
|
|
c469749eaa | ||
|
|
e0009e4267 | ||
|
|
24a06faa3c | ||
|
|
9ede816078 | ||
|
|
646519e074 | ||
|
|
454eff0155 | ||
|
|
ebd7f2929c | ||
|
|
5b36530f67 | ||
|
|
c91b439878 | ||
|
|
7d61f705bf | ||
|
|
9fa8b8440a | ||
|
|
9e8837b37d | ||
|
|
7ca9415322 | ||
|
|
e78c028d20 | ||
|
|
cbc75047b8 | ||
|
|
cc6f6d91e1 | ||
|
|
70e9e1ed3a | ||
|
|
efafdbebaa | ||
|
|
19967ce37c | ||
|
|
ce3d6fba37 | ||
|
|
3e14716fc6 | ||
|
|
43620cd9b6 | ||
|
|
e819140f36 | ||
|
|
f28f0eac1a | ||
|
|
ff4abb8dfe | ||
|
|
e2efaddfed | ||
|
|
b1db6c6fb1 | ||
|
|
279fc469ab | ||
|
|
d185fb6dd7 | ||
|
|
3a667a3809 | ||
|
|
a895775f81 | ||
|
|
0a9beddef9 | ||
|
|
29528e40b0 | ||
|
|
aaf0b702a3 | ||
|
|
9f376db0f4 | ||
|
|
391ce61362 | ||
|
|
62e2ca4c16 | ||
|
|
ea5af73901 | ||
|
|
7d7feda319 | ||
|
|
1c810f68f8 | ||
|
|
8528a80d7e | ||
|
|
dd0bf1a012 | ||
|
|
e0c4752bbf | ||
|
|
c896440525 | ||
|
|
1f042a8434 | ||
|
|
fc3383946d | ||
|
|
4abbebf5e9 | ||
|
|
eb0ad7f22e | ||
|
|
0b1c90718d | ||
|
|
47597b2b7c | ||
|
|
f3c55ba5f2 | ||
|
|
5e453e3227 | ||
|
|
9bf7f45354 | ||
|
|
c8bc511e04 | ||
|
|
85a114e591 | ||
|
|
73b7144394 | ||
|
|
e9f6a0a1d2 | ||
|
|
dfb32d4ebe | ||
|
|
682b0c1729 | ||
|
|
e79abb69eb | ||
|
|
514ed02693 | ||
|
|
ff3241bd57 | ||
|
|
76937bedc9 | ||
|
|
1e65da9aa4 | ||
|
|
0d49377879 | ||
|
|
1a490a8e7a | ||
|
|
e53265472f | ||
|
|
66fdbef90b | ||
|
|
80482e70a8 | ||
|
|
f214c3166b | ||
|
|
4a4303afef | ||
|
|
cc7bdc5b76 | ||
|
|
3539433a9d | ||
|
|
54cb821ae9 | ||
|
|
4924dcfd12 | ||
|
|
d84e76d07c | ||
|
|
eca215d044 | ||
|
|
721de3fba6 | ||
|
|
e7fa98008d | ||
|
|
923fbc54b8 | ||
|
|
ff39f90abd | ||
|
|
db30fa3c5e | ||
|
|
26c902d551 | ||
|
|
e9739f191e | ||
|
|
fa2b97242d | ||
|
|
f1332d4200 | ||
|
|
d34c6df673 | ||
|
|
77ddde1c8a | ||
|
|
d39bdd7ef2 | ||
|
|
473b38ebdf | ||
|
|
59f546804e | ||
|
|
a7f6bdc0ba | ||
|
|
7652f44b79 | ||
|
|
5b3adb2c7e | ||
|
|
ed8f2ab96f | ||
|
|
5fa27a99da | ||
|
|
fcdb97f079 | ||
|
|
f2994bc6d0 | ||
|
|
ffa0bc1733 | ||
|
|
1198005803 | ||
|
|
e3820d1c8e | ||
|
|
af2a01eea2 | ||
|
|
5ec89f4041 | ||
|
|
57efbe4ebc | ||
|
|
7b558c529b | ||
|
|
caabd069d6 | ||
|
|
28ad0345f3 | ||
|
|
d89ae3a2bc | ||
|
|
bd12096e74 | ||
|
|
1cdde4a6b8 | ||
|
|
79704d81c2 | ||
|
|
4225e06db9 | ||
|
|
fc5da31acf | ||
|
|
f2db0309ac | ||
|
|
48dd1a28f8 | ||
|
|
ec04bd99d0 | ||
|
|
899fe62b85 | ||
|
|
512245f54f | ||
|
|
e1cfb04f98 | ||
|
|
4accfad266 | ||
|
|
bf5351d551 | ||
|
|
ef44706957 | ||
|
|
5b0b47635d | ||
|
|
93eeef0131 | ||
|
|
089f0ee30b | ||
|
|
0565e74cfa | ||
|
|
2270f716f2 | ||
|
|
a850d83b33 | ||
|
|
7560b8167b | ||
|
|
5d626b3d07 | ||
|
|
0997440f32 | ||
|
|
62e8d3758a | ||
|
|
3eb1820759 | ||
|
|
bd734cb918 | ||
|
|
6a643f7ca7 | ||
|
|
adef7b76c9 | ||
|
|
1037a8de0d | ||
|
|
3fac9bb1bd | ||
|
|
d825ce9bdf | ||
|
|
25c6a151ce | ||
|
|
1a7add1890 | ||
|
|
fca4194946 | ||
|
|
121101de9d | ||
|
|
a3146926b1 | ||
|
|
81df244d41 | ||
|
|
068b4030f5 | ||
|
|
0916f1179e | ||
|
|
6a5be593df | ||
|
|
7fd1ef80c5 | ||
|
|
26313f7842 | ||
|
|
ce1914419a | ||
|
|
6a828532cb | ||
|
|
e8a002d535 | ||
|
|
4f1e029d0b | ||
|
|
9b93a959e5 | ||
|
|
68d269658f | ||
|
|
875b87b0d6 | ||
|
|
3e79cd7944 | ||
|
|
0ea8972ede | ||
|
|
a7113addd0 | ||
|
|
84df847898 | ||
|
|
333d9373dd | ||
|
|
eb90c5b9aa | ||
|
|
3371e1afff | ||
|
|
8aafb1b729 | ||
|
|
1a0db29aa6 | ||
|
|
52073e7e81 | ||
|
|
4f79c919f0 | ||
|
|
63b265fa04 | ||
|
|
6b01d9d7b9 | ||
|
|
20a0a6724a | ||
|
|
e79cb9e6e0 | ||
|
|
d7ddcc04b5 | ||
|
|
99684a4b2f | ||
|
|
6a6153ca48 | ||
|
|
23a4ff3af6 | ||
|
|
d6c2ba5812 | ||
|
|
fa0237b546 | ||
|
|
9fcaad6b2f | ||
|
|
edc7d56219 | ||
|
|
d0a2b80ecc | ||
|
|
e2d80af81d | ||
|
|
eccd383c1c | ||
|
|
db7a178a8d | ||
|
|
62e0caa950 | ||
|
|
46d8c98aff | ||
|
|
05634ebdb7 | ||
|
|
0e2da62081 | ||
|
|
94d1659ad5 | ||
|
|
0745c92e9a | ||
|
|
7c06952a7d | ||
|
|
e2d8ca3506 | ||
|
|
4f20f537c0 | ||
|
|
a0b42b27b3 | ||
|
|
288fa3979a | ||
|
|
e4cc96ef09 | ||
|
|
60a947f131 | ||
|
|
0226c8da23 | ||
|
|
b0364087ad | ||
|
|
40a9773beb | ||
|
|
790a275443 | ||
|
|
9b9addf3e6 | ||
|
|
57d2437e9c | ||
|
|
a13aea478e | ||
|
|
6def522f38 | ||
|
|
6a63d49823 | ||
|
|
b766cb4ac5 | ||
|
|
32ab1fefa9 | ||
|
|
70761c47eb | ||
|
|
6a09d52b85 | ||
|
|
fcaf23d6bc | ||
|
|
54c2a6458d | ||
|
|
05032ec428 | ||
|
|
e24b905adc | ||
|
|
f27d0c4cd7 | ||
|
|
11a2aa2b4a | ||
|
|
2eee8baa26 | ||
|
|
0949ffc027 | ||
|
|
286538c5d0 | ||
|
|
78844def40 | ||
|
|
e17ce0fb31 | ||
|
|
55a1c297be | ||
|
|
9bb7ae7902 |
44
.github/workflows/build.yml
vendored
@@ -2,15 +2,21 @@ name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: [v*]
|
||||
tags: ['v*', 'test*']
|
||||
|
||||
jobs:
|
||||
build_macos:
|
||||
name: Build for MacOS
|
||||
runs-on: macos-10.15
|
||||
runs-on: macos-13
|
||||
steps:
|
||||
- {name: "Checkout", uses: actions/checkout@v2}
|
||||
- {name: "Checkout gofeed", uses: actions/checkout@v2, with: {repository: nkanaev/gofeed, path: gofeed}}
|
||||
- name: "Checkout"
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '^1.17'
|
||||
- name: Cache Go Modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
@@ -28,10 +34,16 @@ jobs:
|
||||
|
||||
build_windows:
|
||||
name: Build for Windows
|
||||
runs-on: windows-2019
|
||||
runs-on: windows-2022
|
||||
steps:
|
||||
- {name: "Checkout", uses: actions/checkout@v2}
|
||||
- {name: "Checkout gofeed", uses: actions/checkout@v2, with: {repository: nkanaev/gofeed, path: gofeed}}
|
||||
- name: "Checkout"
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '^1.17'
|
||||
- name: Cache Go Modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
@@ -49,11 +61,16 @@ jobs:
|
||||
|
||||
build_linux:
|
||||
name: Build for Linux
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- {name: "Checkout", uses: actions/checkout@v2}
|
||||
- {name: "Checkout gofeed", uses: actions/checkout@v2, with: {repository: nkanaev/gofeed, path: gofeed}}
|
||||
- {name: "Setup Go", uses: actions/setup-go@v2, with: {go-version: '^1.14'}}
|
||||
- name: "Checkout"
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '^1.17'
|
||||
- name: Cache Go Modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
@@ -72,6 +89,7 @@ jobs:
|
||||
create_release:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !contains(github.ref, 'test') }}
|
||||
needs: [build_macos, build_windows, build_linux]
|
||||
steps:
|
||||
- name: Create Release
|
||||
@@ -113,7 +131,7 @@ jobs:
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./yarr-windows.zip
|
||||
asset_name: yarr-${{ github.ref }}-windows32.zip
|
||||
asset_name: yarr-${{ github.ref }}-windows64.zip
|
||||
asset_content_type: application/zip
|
||||
- name: Upload Linux
|
||||
uses: actions/upload-release-asset@v1
|
||||
@@ -122,5 +140,5 @@ jobs:
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./yarr-linux.zip
|
||||
asset_name: yarr-${{ github.ref }}-linux32.zip
|
||||
asset_name: yarr-${{ github.ref }}-linux64.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
2
.gitignore
vendored
@@ -1,5 +1,3 @@
|
||||
/server/assets.go
|
||||
/gofeed
|
||||
/_output
|
||||
/yarr
|
||||
*.db
|
||||
|
||||
|
Before Width: | Height: | Size: 727 KiB |
|
Before Width: | Height: | Size: 395 B |
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-list"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>
|
||||
|
Before Width: | Height: | Size: 482 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-settings"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1011 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-trash-2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
|
||||
|
Before Width: | Height: | Size: 448 B |
@@ -1,375 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>yarr!</title>
|
||||
<link rel="stylesheet" href="./static/stylesheets/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="./static/stylesheets/app.css">
|
||||
<link rel="icon shortcut" href="./static/graphicarts/anchor.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
</head>
|
||||
<body class="theme-light">
|
||||
<div id="app" class="d-flex" :class="{'feed-selected': feedSelected !== null, 'item-selected': itemSelected !== null}" v-cloak>
|
||||
<!-- feed list -->
|
||||
<div id="col-feed-list" class="vh-100 position-relative d-flex flex-column border-right flex-shrink-0" :style="{width: feedListWidth+'px'}">
|
||||
<drag :width="feedListWidth" @resize="resizeFeedList"></drag>
|
||||
<div class="p-2 toolbar d-flex align-items-center">
|
||||
<div class="icon mx-2">{% inline "anchor.svg" %}</div>
|
||||
<div class="flex-grow-1"></div>
|
||||
<button class="toolbar-item"
|
||||
:class="{active: filterSelected == 'unread'}"
|
||||
v-b-tooltip.hover.bottom="'Unread'"
|
||||
@click="filterSelected = 'unread'">
|
||||
<span class="icon">{% inline "circle-full.svg" %}</span>
|
||||
</button>
|
||||
<button class="toolbar-item"
|
||||
:class="{active: filterSelected == 'starred'}"
|
||||
v-b-tooltip.hover.bottom="'Starred'"
|
||||
@click="filterSelected = 'starred'">
|
||||
<span class="icon">{% inline "star-full.svg" %}</span>
|
||||
</button>
|
||||
<button class="toolbar-item"
|
||||
:class="{active: filterSelected == ''}"
|
||||
v-b-tooltip.hover.bottom="'All'"
|
||||
@click="filterSelected = ''">
|
||||
<span class="icon">{% inline "assorted.svg" %}</span>
|
||||
</button>
|
||||
<div class="flex-grow-1"></div>
|
||||
<b-dropdown
|
||||
right no-caret lazy variant="link"
|
||||
class="settings-dropdown"
|
||||
toggle-class="toolbar-item px-2"
|
||||
ref="menuDropdown">
|
||||
<template v-slot:button-content class="toolbar-item">
|
||||
<span class="icon">{% inline "more-horizontal.svg" %}</span>
|
||||
</template>
|
||||
<b-dropdown-item-button @click="showSettings('create')">
|
||||
<span class="icon mr-1">{% inline "plus.svg" %}</span>
|
||||
New Feed
|
||||
</b-dropdown-item-button>
|
||||
<b-dropdown-item-button @click.stop="showSettings('manage')">
|
||||
<span class="icon mr-1">{% inline "list.svg" %}</span>
|
||||
Manage Feeds
|
||||
</b-dropdown-item-button>
|
||||
<b-dropdown-divider></b-dropdown-divider>
|
||||
<b-dropdown-item-button @click.stop="fetchAllFeeds()">
|
||||
<span class="icon mr-1">{% inline "rotate-cw.svg" %}</span>
|
||||
Refresh Feeds
|
||||
</b-dropdown-item-button>
|
||||
<b-dropdown-divider></b-dropdown-divider>
|
||||
<b-dropdown-header>Sort by</b-dropdown-header>
|
||||
<b-dropdown-item-button @click.stop="itemSortNewestFirst=true">
|
||||
<span class="icon mr-1" :class="{invisible: !itemSortNewestFirst}">{% inline "check.svg" %}</span>
|
||||
Newest First
|
||||
</b-dropdown-item-button>
|
||||
<b-dropdown-item-button @click="itemSortNewestFirst=false">
|
||||
<span class="icon mr-1" :class="{invisible: itemSortNewestFirst}">{% inline "check.svg" %}</span>
|
||||
Oldest First
|
||||
</b-dropdown-item-button>
|
||||
<b-dropdown-divider></b-dropdown-divider>
|
||||
<b-dropdown-header>Subscriptions</b-dropdown-header>
|
||||
<b-dropdown-form id="opml-import-form" enctype="multipart/form-data">
|
||||
<input type="file"
|
||||
id="opml-import"
|
||||
@change="importOPML"
|
||||
name="opml"
|
||||
style="opacity: 0; width: 1px; height: 0; position: absolute;">
|
||||
<label class="dropdown-item mb-0 cursor-pointer" for="opml-import">
|
||||
<span class="icon mr-1">{% inline "download.svg" %}</span>
|
||||
Import
|
||||
</label>
|
||||
</b-dropdown-form>
|
||||
<b-dropdown-item href="/opml/export">
|
||||
<span class="icon mr-1">{% inline "upload.svg" %}</span>
|
||||
Export
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
<div class="p-2 overflow-auto border-top flex-grow-1">
|
||||
<label class="selectgroup">
|
||||
<input type="radio" name="feed" value="" v-model="feedSelected">
|
||||
<div class="selectgroup-label d-flex align-items-center w-100">
|
||||
<span class="icon mr-2">{% inline "layers.svg" %}</span>
|
||||
<span class="flex-fill text-left text-truncate" v-if="filterSelected=='unread'">All Unread</span>
|
||||
<span class="flex-fill text-left text-truncate" v-if="filterSelected=='starred'">All Starred</span>
|
||||
<span class="flex-fill text-left text-truncate" v-if="filterSelected==''">All Feeds</span>
|
||||
<span class="counter text-right">{{ filteredTotalStats }}</span>
|
||||
</div>
|
||||
</label>
|
||||
<div v-for="folder in foldersWithFeeds">
|
||||
<label class="selectgroup mt-1"
|
||||
:class="{'d-none': filterSelected
|
||||
&& !filteredFolderStats[folder.id]
|
||||
&& (!itemSelected || feedsById[itemSelectedDetails.feed_id].folder_id != folder.id)}">
|
||||
<input type="radio" name="feed" :value="'folder:'+folder.id" v-model="feedSelected">
|
||||
<div class="selectgroup-label d-flex align-items-center w-100" v-if="folder.id">
|
||||
<span class="icon mr-2"
|
||||
:class="{expanded: folder.is_expanded}"
|
||||
@click.prevent="toggleFolderExpanded(folder)">
|
||||
{% inline "chevron-right.svg" %}
|
||||
</span>
|
||||
<span class="flex-fill text-left text-truncate">{{ folder.title }}</span>
|
||||
<span class="counter text-right">{{ filteredFolderStats[folder.id] || '' }}</span>
|
||||
</div>
|
||||
</label>
|
||||
<div v-show="!folder.id || folder.is_expanded" class="mt-1" :class="{'pl-3': folder.id}">
|
||||
<label class="selectgroup"
|
||||
:class="{'d-none': filterSelected
|
||||
&& !filteredFeedStats[feed.id]
|
||||
&& (!itemSelected || itemSelectedDetails.feed_id != feed.id)}"
|
||||
v-for="feed in folder.feeds">
|
||||
<input type="radio" name="feed" :value="'feed:'+feed.id" v-model="feedSelected">
|
||||
<div class="selectgroup-label d-flex align-items-center w-100">
|
||||
<span class="icon mr-2" v-if="!feed.has_icon">{% inline "rss.svg" %}</span>
|
||||
<span class="icon mr-2" v-else><img v-lazy="'/api/feeds/'+feed.id+'/icon'" alt=""></span>
|
||||
<span class="flex-fill text-left text-truncate">{{ feed.title }}</span>
|
||||
<span class="counter text-right">{{ filteredFeedStats[feed.id] || '' }}</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-2 toolbar d-flex align-items-center border-top flex-shrink-0" v-if="loading.feeds">
|
||||
<span class="icon loading mx-2"></span>
|
||||
<span class="text-truncate cursor-default noselect">Refreshing ({{ loading.feeds }} left)</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- item list -->
|
||||
<div id="col-item-list" class="vh-100 position-relative d-flex flex-column border-right flex-shrink-0" :style="{width: itemListWidth+'px'}">
|
||||
<drag :width="itemListWidth" @resize="resizeItemList"></drag>
|
||||
<div class="px-2 toolbar d-flex align-items-center">
|
||||
<button class="toolbar-item mr-2 d-block d-md-none"
|
||||
@click="feedSelected = null"
|
||||
v-b-tooltip.hover.bottom="'Show Feeds'">
|
||||
<span class="icon">{% inline "chevron-left.svg" %}</span>
|
||||
</button>
|
||||
<div class="input-icon flex-grow-1">
|
||||
<span class="icon">{% inline "search.svg" %}</span>
|
||||
<input class="d-block toolbar-search" type="" v-model="itemSearch">
|
||||
</div>
|
||||
<button class="toolbar-item ml-2"
|
||||
@click="markItemsRead()"
|
||||
v-if="filterSelected == 'unread'"
|
||||
v-b-tooltip.hover.bottom="'Mark All Read'">
|
||||
<span class="icon">{% inline "check.svg" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-2 overflow-auto border-top flex-grow-1" v-scroll="loadMoreItems" ref="itemlist">
|
||||
<label v-for="item in items" :key="item.id"
|
||||
class="selectgroup">
|
||||
<input type="radio" name="item" :value="item.id" v-model="itemSelected">
|
||||
<div class="selectgroup-label d-flex flex-column">
|
||||
<div style="line-height: 1; opacity: .7; margin-bottom: .1rem;" class="d-flex align-items-center">
|
||||
<transition name="indicator">
|
||||
<span class="icon icon-small mr-1" v-if="item.status=='unread'">{% inline "circle-full.svg" %}</span>
|
||||
<span class="icon icon-small mr-1" v-if="item.status=='starred'">{% inline "star-full.svg" %}</span>
|
||||
</transition>
|
||||
<small class="flex-fill text-truncate mr-1">
|
||||
{{ feedsById[item.feed_id].title }}
|
||||
</small>
|
||||
<small class="flex-shrink-0"><relative-time :val="item.date"/></small>
|
||||
</div>
|
||||
<div>{{ item.title || 'untitled' }}</div>
|
||||
</div>
|
||||
</label>
|
||||
<button class="btn btn-link btn-block loading my-3" v-if="itemsPage.cur < itemsPage.num"></button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- item show -->
|
||||
<div id="col-item" class="vh-100 d-flex flex-column w-100" style="min-width: 0;">
|
||||
<div class="toolbar px-2 d-flex align-items-center" v-if="itemSelected">
|
||||
<button class="toolbar-item"
|
||||
@click="toggleItemStarred(itemSelectedDetails)"
|
||||
v-b-tooltip.hover.bottom="'Mark Starred'">
|
||||
<span class="icon" v-if="itemSelectedDetails.status=='starred'" >{% inline "star-full.svg" %}</span>
|
||||
<span class="icon" v-else-if="itemSelectedDetails.status!='starred'" >{% inline "star.svg" %}</span>
|
||||
</button>
|
||||
<button class="toolbar-item"
|
||||
:disabled="itemSelectedDetails.status=='starred'"
|
||||
v-b-tooltip.hover.bottom="'Mark Unread'"
|
||||
@click="toggleItemRead(itemSelectedDetails)">
|
||||
<span class="icon" v-if="itemSelectedDetails.status=='unread'">{% inline "circle-full.svg" %}</span>
|
||||
<span class="icon" v-if="itemSelectedDetails.status!='unread'">{% inline "circle.svg" %}</span>
|
||||
</button>
|
||||
<a class="toolbar-item" id="content-appearance" v-b-tooltip.hover.bottom="'Appearance'" tabindex="0">
|
||||
<span class="icon">{% inline "sliders.svg" %}</span>
|
||||
</a>
|
||||
<button class="toolbar-item"
|
||||
:class="{active: itemSelectedReadability}"
|
||||
@click="getReadable(itemSelectedDetails)"
|
||||
v-b-tooltip.hover.bottom="'Read Here'">
|
||||
<span class="icon" :class="{'icon-loading': loading.readability}">{% inline "book-open.svg" %}</span>
|
||||
</button>
|
||||
<a class="toolbar-item" :href="itemSelectedDetails.link" target="_blank" v-b-tooltip.hover.bottom="'Open Link'">
|
||||
<span class="icon">{% inline "external-link.svg" %}</span>
|
||||
</a>
|
||||
<b-popover target="content-appearance" triggers="focus" placement="bottom">
|
||||
<div class="p-1" style="width: 200px;">
|
||||
<div class="d-flex">
|
||||
<label class="themepicker">
|
||||
<input type="radio" name="settingsTheme" value="light" v-model="theme.name">
|
||||
<div class="themepicker-label appearance-option"></div>
|
||||
</label>
|
||||
<label class="themepicker">
|
||||
<input type="radio" name="settingsTheme" value="sepia" v-model="theme.name">
|
||||
<div class="themepicker-label appearance-option"></div>
|
||||
</label>
|
||||
<label class="themepicker">
|
||||
<input type="radio" name="settingsTheme" value="night" v-model="theme.name">
|
||||
<div class="themepicker-label appearance-option"></div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<label class="selectgroup">
|
||||
<input type="radio" name="font" value="" v-model="theme.font">
|
||||
<div class="selectgroup-label appearance-option">
|
||||
System Default
|
||||
</div>
|
||||
</label>
|
||||
<label class="selectgroup" v-for="f in fonts" :key="f">
|
||||
<input type="radio" name="font" :value="f" v-model="theme.font">
|
||||
<div class="selectgroup-label appearance-option":style="{'font-family': f}">
|
||||
{{ f }}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="btn-group d-flex mt-2">
|
||||
<button class="btn btn-outline appearance-option"
|
||||
style="font-size: 0.8rem" @click="incrFont(-1)">A</button>
|
||||
<button class="btn btn-outline appearance-option"
|
||||
style="font-size: 1.2rem" @click="incrFont(1)">A</button>
|
||||
</div>
|
||||
</div>
|
||||
</b-popover>
|
||||
<div class="flex-grow-1"></div>
|
||||
<button class="toolbar-item" @click="itemSelected=null" v-b-tooltip.hover.bottom="'Close Article'">
|
||||
<span class="icon">{% inline "x.svg" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="itemSelected"
|
||||
ref="content"
|
||||
class="content px-4 pt-3 pb-5 border-top overflow-auto"
|
||||
:style="{'font-family': theme.font, 'font-size': theme.size + 'rem'}">
|
||||
<h1><b>{{ itemSelectedDetails.title }}</b></h1>
|
||||
<div class="text-muted">
|
||||
<div>{{ feedsById[itemSelectedDetails.feed_id].title }}</div>
|
||||
<time>{{ formatDate(itemSelectedDetails.date) }}</time>
|
||||
</div>
|
||||
<hr>
|
||||
<div v-html="itemSelectedContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
<b-modal id="settings-modal" hide-header hide-footer lazy>
|
||||
<button class="btn btn-link outline-none float-right p-2 mr-n2 mt-n2" style="line-height: 1" @click="$bvModal.hide('settings-modal')">
|
||||
<span class="icon">{% inline "x.svg" %}</span>
|
||||
</button>
|
||||
<div v-if="settings=='create'">
|
||||
<p class="cursor-default"><b>New Feed</b></p>
|
||||
<form action="" @submit.prevent="createFeed(event)" class="mt-4">
|
||||
<label for="feed-url">URL</label>
|
||||
<input id="feed-url" name="url" type="url" class="form-control" required autocomplete="off" :readonly="feedNewChoice.length > 0">
|
||||
<label for="feed-folder" class="mt-3 d-block">
|
||||
Folder
|
||||
<a href="#" class="float-right text-decoration-none" @click.prevent="createNewFeedFolder()">new folder</a>
|
||||
</label>
|
||||
<select class="form-control" id="feed-folder" name="folder_id" ref="newFeedFolder">
|
||||
<option value="">---</option>
|
||||
<option :value="folder.id" v-for="folder in folders">{{ folder.title }}</option>
|
||||
</select>
|
||||
<div class="mt-4" v-if="feedNewChoice.length">
|
||||
<p class="mb-2">
|
||||
Multiple feeds found. Choose one below:
|
||||
<a href="#" class="float-right text-decoration-none" @click.prevent="resetFeedChoice()">cancel</a>
|
||||
</p>
|
||||
<label class="selectgroup" v-for="choice in feedNewChoice">
|
||||
<input type="radio" name="feedToAdd" :value="choice.url" v-model="feedNewChoiceSelected">
|
||||
<div class="selectgroup-label">
|
||||
<div class="text-truncate">{{ choice.title }}</div>
|
||||
<div class="text-truncate" :class="{light: choice.title}">{{ choice.url }}</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<button class="btn btn-block btn-default mt-3" :class="{loading: loading.newfeed}" type="submit">Add</button>
|
||||
</form>
|
||||
</div>
|
||||
<div v-else-if="settings=='manage'">
|
||||
<p class="cursor-default"><b>Manage Feeds</b></p>
|
||||
<div v-for="folder in foldersWithFeeds" class="mt-4" :key="folder.id">
|
||||
<div class="list-row d-flex align-items-center">
|
||||
<div class="w-100 text-truncate" v-if="folder.id">
|
||||
<span class="icon mr-2">{% inline "folder.svg" %}</span>
|
||||
{{ folder.title }}
|
||||
</div>
|
||||
<div class="flex-shrink-0" v-if="folder.id">
|
||||
<b-dropdown right no-caret lazy variant="link" class="settings-dropdown" toggle-class="text-decoration-none">
|
||||
<template v-slot:button-content>
|
||||
<span class="icon">{% inline "more-vertical.svg" %}</span>
|
||||
</template>
|
||||
<b-dropdown-header>{{ folder.title }}</b-dropdown-header>
|
||||
<b-dropdown-item @click.prevent="renameFolder(folder)">Rename</b-dropdown-item>
|
||||
<b-dropdown-divider></b-dropdown-divider>
|
||||
<b-dropdown-item class="dropdown-danger"
|
||||
@click.prevent="deleteFolder(folder)">
|
||||
Delete
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="feed in folder.feeds" class="list-row d-flex align-items-center" :key="feed.id">
|
||||
<div class="w-100 text-truncate">
|
||||
<span class="icon mr-2" v-if="!feed.has_icon">{% inline "rss.svg" %}</span>
|
||||
<span class="icon mr-2" v-else><img v-lazy="'/api/feeds/'+feed.id+'/icon'" alt=""></span>
|
||||
{{ feed.title }}
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<b-dropdown right no-caret lazy variant="link" class="settings-dropdown" toggle-class="text-decoration-none">
|
||||
<template v-slot:button-content>
|
||||
<span class="icon">{% inline "more-vertical.svg" %}</span>
|
||||
</template>
|
||||
<b-dropdown-header>{{ feed.title }}</b-dropdown-header>
|
||||
<b-dropdown-item :href="feed.link" target="_blank" v-if="feed.link">Visit Website</b-dropdown-item>
|
||||
<b-dropdown-divider v-if="feed.link"></b-dropdown-divider>
|
||||
<b-dropdown-item @click.prevent="renameFeed(feed)">Rename</b-dropdown-item>
|
||||
<b-dropdown-divider v-if="folders.length"></b-dropdown-divider>
|
||||
<b-dropdown-header v-if="folders.length">Move to...</b-dropdown-header>
|
||||
<b-dropdown-item @click="moveFeed(feed, null)" v-if="feed.folder_id">
|
||||
---
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item-button
|
||||
v-if="folder.id != feed.folder_id"
|
||||
v-for="folder in folders"
|
||||
@click="moveFeed(feed, folder)">
|
||||
<span class="icon mr-1">{% inline "folder.svg" %}</span>
|
||||
{{ folder.title }}
|
||||
</b-dropdown-item-button>
|
||||
<b-dropdown-item-button @click="moveFeedToNewFolder(feed)">
|
||||
<span class="text-muted icon mr-1">{% inline "plus.svg" %}</span>
|
||||
<span class="text-muted">New Folder</span>
|
||||
</b-dropdown-item-button>
|
||||
<b-dropdown-divider></b-dropdown-divider>
|
||||
<b-dropdown-item class="dropdown-danger"
|
||||
@click.prevent="deleteFeed(feed)">
|
||||
Delete
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- polyfill -->
|
||||
<script src="./static/javascripts/fetch.umd.js"></script>
|
||||
<script src="./static/javascripts/url-polyfill.min.js"></script>
|
||||
<!-- external -->
|
||||
<script src="./static/javascripts/vue.min.js"></script>
|
||||
<script src="./static/javascripts/vue-lazyload.js"></script>
|
||||
<script src="./static/javascripts/popper.min.js"></script>
|
||||
<script src="./static/javascripts/bootstrap-vue.min.js"></script>
|
||||
<script src="./static/javascripts/Readability.js"></script>
|
||||
<script src="./static/javascripts/purify.min.js"></script>
|
||||
<!-- internal -->
|
||||
<script src="./static/javascripts/api.js"></script>
|
||||
<script src="./static/javascripts/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,95 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
var api = function(method, endpoint, data) {
|
||||
return fetch(endpoint, {
|
||||
method: method,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
}
|
||||
|
||||
var json = function(res) {
|
||||
return res.json()
|
||||
}
|
||||
|
||||
var param = function(query) {
|
||||
if (!query) return ''
|
||||
return '?' + Object.keys(query).map(function(key) {
|
||||
return encodeURIComponent(key) + '=' + encodeURIComponent(query[key])
|
||||
}).join('&')
|
||||
}
|
||||
|
||||
window.api = {
|
||||
feeds: {
|
||||
list: function() {
|
||||
return api('get', '/api/feeds').then(json)
|
||||
},
|
||||
create: function(data) {
|
||||
return api('post', '/api/feeds', data).then(json)
|
||||
},
|
||||
update: function(id, data) {
|
||||
return api('put', '/api/feeds/' + id, data)
|
||||
},
|
||||
delete: function(id) {
|
||||
return api('delete', '/api/feeds/' + id)
|
||||
},
|
||||
list_items: function(id) {
|
||||
return api('get', '/api/feeds/' + id + '/items').then(json)
|
||||
},
|
||||
refresh: function() {
|
||||
return api('post', '/api/feeds/refresh')
|
||||
},
|
||||
},
|
||||
folders: {
|
||||
list: function() {
|
||||
return api('get', '/api/folders').then(json)
|
||||
},
|
||||
create: function(data) {
|
||||
return api('post', '/api/folders', data).then(json)
|
||||
},
|
||||
update: function(id, data) {
|
||||
return api('put', '/api/folders/' + id, data)
|
||||
},
|
||||
delete: function(id) {
|
||||
return api('delete', '/api/folders/' + id)
|
||||
},
|
||||
list_items: function(id) {
|
||||
return api('get', '/api/folders/' + id + '/items').then(json)
|
||||
}
|
||||
},
|
||||
items: {
|
||||
list: function(query) {
|
||||
return api('get', '/api/items' + param(query)).then(json)
|
||||
},
|
||||
update: function(id, data) {
|
||||
return api('put', '/api/items/' + id, data)
|
||||
},
|
||||
mark_read: function(query) {
|
||||
return api('put', '/api/items' + param(query))
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
get: function() {
|
||||
return api('get', '/api/settings').then(json)
|
||||
},
|
||||
update: function(data) {
|
||||
return api('put', '/api/settings', data)
|
||||
},
|
||||
},
|
||||
status: function() {
|
||||
return api('get', '/api/status').then(json)
|
||||
},
|
||||
upload_opml: function(form) {
|
||||
return fetch('/opml/import', {
|
||||
method: 'post',
|
||||
body: new FormData(form),
|
||||
})
|
||||
},
|
||||
crawl: function(url) {
|
||||
return fetch('/page?url=' + url).then(function(res) {
|
||||
return res.text()
|
||||
})
|
||||
}
|
||||
}
|
||||
})()
|
||||
11
assets/javascripts/bootstrap-vue.min.js
vendored
@@ -1,609 +0,0 @@
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
||||
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
||||
(factory((global.WHATWGFetch = {})));
|
||||
}(this, (function (exports) { 'use strict';
|
||||
|
||||
var global = (typeof self !== 'undefined' && self) || (typeof global !== 'undefined' && global);
|
||||
|
||||
var support = {
|
||||
searchParams: 'URLSearchParams' in global,
|
||||
iterable: 'Symbol' in global && 'iterator' in Symbol,
|
||||
blob:
|
||||
'FileReader' in global &&
|
||||
'Blob' in global &&
|
||||
(function() {
|
||||
try {
|
||||
new Blob();
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
})(),
|
||||
formData: 'FormData' in global,
|
||||
arrayBuffer: 'ArrayBuffer' in global
|
||||
};
|
||||
|
||||
function isDataView(obj) {
|
||||
return obj && DataView.prototype.isPrototypeOf(obj)
|
||||
}
|
||||
|
||||
if (support.arrayBuffer) {
|
||||
var viewClasses = [
|
||||
'[object Int8Array]',
|
||||
'[object Uint8Array]',
|
||||
'[object Uint8ClampedArray]',
|
||||
'[object Int16Array]',
|
||||
'[object Uint16Array]',
|
||||
'[object Int32Array]',
|
||||
'[object Uint32Array]',
|
||||
'[object Float32Array]',
|
||||
'[object Float64Array]'
|
||||
];
|
||||
|
||||
var isArrayBufferView =
|
||||
ArrayBuffer.isView ||
|
||||
function(obj) {
|
||||
return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeName(name) {
|
||||
if (typeof name !== 'string') {
|
||||
name = String(name);
|
||||
}
|
||||
if (/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(name) || name === '') {
|
||||
throw new TypeError('Invalid character in header field name')
|
||||
}
|
||||
return name.toLowerCase()
|
||||
}
|
||||
|
||||
function normalizeValue(value) {
|
||||
if (typeof value !== 'string') {
|
||||
value = String(value);
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// Build a destructive iterator for the value list
|
||||
function iteratorFor(items) {
|
||||
var iterator = {
|
||||
next: function() {
|
||||
var value = items.shift();
|
||||
return {done: value === undefined, value: value}
|
||||
}
|
||||
};
|
||||
|
||||
if (support.iterable) {
|
||||
iterator[Symbol.iterator] = function() {
|
||||
return iterator
|
||||
};
|
||||
}
|
||||
|
||||
return iterator
|
||||
}
|
||||
|
||||
function Headers(headers) {
|
||||
this.map = {};
|
||||
|
||||
if (headers instanceof Headers) {
|
||||
headers.forEach(function(value, name) {
|
||||
this.append(name, value);
|
||||
}, this);
|
||||
} else if (Array.isArray(headers)) {
|
||||
headers.forEach(function(header) {
|
||||
this.append(header[0], header[1]);
|
||||
}, this);
|
||||
} else if (headers) {
|
||||
Object.getOwnPropertyNames(headers).forEach(function(name) {
|
||||
this.append(name, headers[name]);
|
||||
}, this);
|
||||
}
|
||||
}
|
||||
|
||||
Headers.prototype.append = function(name, value) {
|
||||
name = normalizeName(name);
|
||||
value = normalizeValue(value);
|
||||
var oldValue = this.map[name];
|
||||
this.map[name] = oldValue ? oldValue + ', ' + value : value;
|
||||
};
|
||||
|
||||
Headers.prototype['delete'] = function(name) {
|
||||
delete this.map[normalizeName(name)];
|
||||
};
|
||||
|
||||
Headers.prototype.get = function(name) {
|
||||
name = normalizeName(name);
|
||||
return this.has(name) ? this.map[name] : null
|
||||
};
|
||||
|
||||
Headers.prototype.has = function(name) {
|
||||
return this.map.hasOwnProperty(normalizeName(name))
|
||||
};
|
||||
|
||||
Headers.prototype.set = function(name, value) {
|
||||
this.map[normalizeName(name)] = normalizeValue(value);
|
||||
};
|
||||
|
||||
Headers.prototype.forEach = function(callback, thisArg) {
|
||||
for (var name in this.map) {
|
||||
if (this.map.hasOwnProperty(name)) {
|
||||
callback.call(thisArg, this.map[name], name, this);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Headers.prototype.keys = function() {
|
||||
var items = [];
|
||||
this.forEach(function(value, name) {
|
||||
items.push(name);
|
||||
});
|
||||
return iteratorFor(items)
|
||||
};
|
||||
|
||||
Headers.prototype.values = function() {
|
||||
var items = [];
|
||||
this.forEach(function(value) {
|
||||
items.push(value);
|
||||
});
|
||||
return iteratorFor(items)
|
||||
};
|
||||
|
||||
Headers.prototype.entries = function() {
|
||||
var items = [];
|
||||
this.forEach(function(value, name) {
|
||||
items.push([name, value]);
|
||||
});
|
||||
return iteratorFor(items)
|
||||
};
|
||||
|
||||
if (support.iterable) {
|
||||
Headers.prototype[Symbol.iterator] = Headers.prototype.entries;
|
||||
}
|
||||
|
||||
function consumed(body) {
|
||||
if (body.bodyUsed) {
|
||||
return Promise.reject(new TypeError('Already read'))
|
||||
}
|
||||
body.bodyUsed = true;
|
||||
}
|
||||
|
||||
function fileReaderReady(reader) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
reader.onload = function() {
|
||||
resolve(reader.result);
|
||||
};
|
||||
reader.onerror = function() {
|
||||
reject(reader.error);
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
function readBlobAsArrayBuffer(blob) {
|
||||
var reader = new FileReader();
|
||||
var promise = fileReaderReady(reader);
|
||||
reader.readAsArrayBuffer(blob);
|
||||
return promise
|
||||
}
|
||||
|
||||
function readBlobAsText(blob) {
|
||||
var reader = new FileReader();
|
||||
var promise = fileReaderReady(reader);
|
||||
reader.readAsText(blob);
|
||||
return promise
|
||||
}
|
||||
|
||||
function readArrayBufferAsText(buf) {
|
||||
var view = new Uint8Array(buf);
|
||||
var chars = new Array(view.length);
|
||||
|
||||
for (var i = 0; i < view.length; i++) {
|
||||
chars[i] = String.fromCharCode(view[i]);
|
||||
}
|
||||
return chars.join('')
|
||||
}
|
||||
|
||||
function bufferClone(buf) {
|
||||
if (buf.slice) {
|
||||
return buf.slice(0)
|
||||
} else {
|
||||
var view = new Uint8Array(buf.byteLength);
|
||||
view.set(new Uint8Array(buf));
|
||||
return view.buffer
|
||||
}
|
||||
}
|
||||
|
||||
function Body() {
|
||||
this.bodyUsed = false;
|
||||
|
||||
this._initBody = function(body) {
|
||||
/*
|
||||
fetch-mock wraps the Response object in an ES6 Proxy to
|
||||
provide useful test harness features such as flush. However, on
|
||||
ES5 browsers without fetch or Proxy support pollyfills must be used;
|
||||
the proxy-pollyfill is unable to proxy an attribute unless it exists
|
||||
on the object before the Proxy is created. This change ensures
|
||||
Response.bodyUsed exists on the instance, while maintaining the
|
||||
semantic of setting Request.bodyUsed in the constructor before
|
||||
_initBody is called.
|
||||
*/
|
||||
this.bodyUsed = this.bodyUsed;
|
||||
this._bodyInit = body;
|
||||
if (!body) {
|
||||
this._bodyText = '';
|
||||
} else if (typeof body === 'string') {
|
||||
this._bodyText = body;
|
||||
} else if (support.blob && Blob.prototype.isPrototypeOf(body)) {
|
||||
this._bodyBlob = body;
|
||||
} else if (support.formData && FormData.prototype.isPrototypeOf(body)) {
|
||||
this._bodyFormData = body;
|
||||
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
|
||||
this._bodyText = body.toString();
|
||||
} else if (support.arrayBuffer && support.blob && isDataView(body)) {
|
||||
this._bodyArrayBuffer = bufferClone(body.buffer);
|
||||
// IE 10-11 can't handle a DataView body.
|
||||
this._bodyInit = new Blob([this._bodyArrayBuffer]);
|
||||
} else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) {
|
||||
this._bodyArrayBuffer = bufferClone(body);
|
||||
} else {
|
||||
this._bodyText = body = Object.prototype.toString.call(body);
|
||||
}
|
||||
|
||||
if (!this.headers.get('content-type')) {
|
||||
if (typeof body === 'string') {
|
||||
this.headers.set('content-type', 'text/plain;charset=UTF-8');
|
||||
} else if (this._bodyBlob && this._bodyBlob.type) {
|
||||
this.headers.set('content-type', this._bodyBlob.type);
|
||||
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
|
||||
this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (support.blob) {
|
||||
this.blob = function() {
|
||||
var rejected = consumed(this);
|
||||
if (rejected) {
|
||||
return rejected
|
||||
}
|
||||
|
||||
if (this._bodyBlob) {
|
||||
return Promise.resolve(this._bodyBlob)
|
||||
} else if (this._bodyArrayBuffer) {
|
||||
return Promise.resolve(new Blob([this._bodyArrayBuffer]))
|
||||
} else if (this._bodyFormData) {
|
||||
throw new Error('could not read FormData body as blob')
|
||||
} else {
|
||||
return Promise.resolve(new Blob([this._bodyText]))
|
||||
}
|
||||
};
|
||||
|
||||
this.arrayBuffer = function() {
|
||||
if (this._bodyArrayBuffer) {
|
||||
var isConsumed = consumed(this);
|
||||
if (isConsumed) {
|
||||
return isConsumed
|
||||
}
|
||||
if (ArrayBuffer.isView(this._bodyArrayBuffer)) {
|
||||
return Promise.resolve(
|
||||
this._bodyArrayBuffer.buffer.slice(
|
||||
this._bodyArrayBuffer.byteOffset,
|
||||
this._bodyArrayBuffer.byteOffset + this._bodyArrayBuffer.byteLength
|
||||
)
|
||||
)
|
||||
} else {
|
||||
return Promise.resolve(this._bodyArrayBuffer)
|
||||
}
|
||||
} else {
|
||||
return this.blob().then(readBlobAsArrayBuffer)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
this.text = function() {
|
||||
var rejected = consumed(this);
|
||||
if (rejected) {
|
||||
return rejected
|
||||
}
|
||||
|
||||
if (this._bodyBlob) {
|
||||
return readBlobAsText(this._bodyBlob)
|
||||
} else if (this._bodyArrayBuffer) {
|
||||
return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer))
|
||||
} else if (this._bodyFormData) {
|
||||
throw new Error('could not read FormData body as text')
|
||||
} else {
|
||||
return Promise.resolve(this._bodyText)
|
||||
}
|
||||
};
|
||||
|
||||
if (support.formData) {
|
||||
this.formData = function() {
|
||||
return this.text().then(decode)
|
||||
};
|
||||
}
|
||||
|
||||
this.json = function() {
|
||||
return this.text().then(JSON.parse)
|
||||
};
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
// HTTP methods whose capitalization should be normalized
|
||||
var methods = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT'];
|
||||
|
||||
function normalizeMethod(method) {
|
||||
var upcased = method.toUpperCase();
|
||||
return methods.indexOf(upcased) > -1 ? upcased : method
|
||||
}
|
||||
|
||||
function Request(input, options) {
|
||||
if (!(this instanceof Request)) {
|
||||
throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.')
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
var body = options.body;
|
||||
|
||||
if (input instanceof Request) {
|
||||
if (input.bodyUsed) {
|
||||
throw new TypeError('Already read')
|
||||
}
|
||||
this.url = input.url;
|
||||
this.credentials = input.credentials;
|
||||
if (!options.headers) {
|
||||
this.headers = new Headers(input.headers);
|
||||
}
|
||||
this.method = input.method;
|
||||
this.mode = input.mode;
|
||||
this.signal = input.signal;
|
||||
if (!body && input._bodyInit != null) {
|
||||
body = input._bodyInit;
|
||||
input.bodyUsed = true;
|
||||
}
|
||||
} else {
|
||||
this.url = String(input);
|
||||
}
|
||||
|
||||
this.credentials = options.credentials || this.credentials || 'same-origin';
|
||||
if (options.headers || !this.headers) {
|
||||
this.headers = new Headers(options.headers);
|
||||
}
|
||||
this.method = normalizeMethod(options.method || this.method || 'GET');
|
||||
this.mode = options.mode || this.mode || null;
|
||||
this.signal = options.signal || this.signal;
|
||||
this.referrer = null;
|
||||
|
||||
if ((this.method === 'GET' || this.method === 'HEAD') && body) {
|
||||
throw new TypeError('Body not allowed for GET or HEAD requests')
|
||||
}
|
||||
this._initBody(body);
|
||||
|
||||
if (this.method === 'GET' || this.method === 'HEAD') {
|
||||
if (options.cache === 'no-store' || options.cache === 'no-cache') {
|
||||
// Search for a '_' parameter in the query string
|
||||
var reParamSearch = /([?&])_=[^&]*/;
|
||||
if (reParamSearch.test(this.url)) {
|
||||
// If it already exists then set the value with the current time
|
||||
this.url = this.url.replace(reParamSearch, '$1_=' + new Date().getTime());
|
||||
} else {
|
||||
// Otherwise add a new '_' parameter to the end with the current time
|
||||
var reQueryString = /\?/;
|
||||
this.url += (reQueryString.test(this.url) ? '&' : '?') + '_=' + new Date().getTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Request.prototype.clone = function() {
|
||||
return new Request(this, {body: this._bodyInit})
|
||||
};
|
||||
|
||||
function decode(body) {
|
||||
var form = new FormData();
|
||||
body
|
||||
.trim()
|
||||
.split('&')
|
||||
.forEach(function(bytes) {
|
||||
if (bytes) {
|
||||
var split = bytes.split('=');
|
||||
var name = split.shift().replace(/\+/g, ' ');
|
||||
var value = split.join('=').replace(/\+/g, ' ');
|
||||
form.append(decodeURIComponent(name), decodeURIComponent(value));
|
||||
}
|
||||
});
|
||||
return form
|
||||
}
|
||||
|
||||
function parseHeaders(rawHeaders) {
|
||||
var headers = new Headers();
|
||||
// Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space
|
||||
// https://tools.ietf.org/html/rfc7230#section-3.2
|
||||
var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, ' ');
|
||||
preProcessedHeaders.split(/\r?\n/).forEach(function(line) {
|
||||
var parts = line.split(':');
|
||||
var key = parts.shift().trim();
|
||||
if (key) {
|
||||
var value = parts.join(':').trim();
|
||||
headers.append(key, value);
|
||||
}
|
||||
});
|
||||
return headers
|
||||
}
|
||||
|
||||
Body.call(Request.prototype);
|
||||
|
||||
function Response(bodyInit, options) {
|
||||
if (!(this instanceof Response)) {
|
||||
throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.')
|
||||
}
|
||||
if (!options) {
|
||||
options = {};
|
||||
}
|
||||
|
||||
this.type = 'default';
|
||||
this.status = options.status === undefined ? 200 : options.status;
|
||||
this.ok = this.status >= 200 && this.status < 300;
|
||||
this.statusText = 'statusText' in options ? options.statusText : '';
|
||||
this.headers = new Headers(options.headers);
|
||||
this.url = options.url || '';
|
||||
this._initBody(bodyInit);
|
||||
}
|
||||
|
||||
Body.call(Response.prototype);
|
||||
|
||||
Response.prototype.clone = function() {
|
||||
return new Response(this._bodyInit, {
|
||||
status: this.status,
|
||||
statusText: this.statusText,
|
||||
headers: new Headers(this.headers),
|
||||
url: this.url
|
||||
})
|
||||
};
|
||||
|
||||
Response.error = function() {
|
||||
var response = new Response(null, {status: 0, statusText: ''});
|
||||
response.type = 'error';
|
||||
return response
|
||||
};
|
||||
|
||||
var redirectStatuses = [301, 302, 303, 307, 308];
|
||||
|
||||
Response.redirect = function(url, status) {
|
||||
if (redirectStatuses.indexOf(status) === -1) {
|
||||
throw new RangeError('Invalid status code')
|
||||
}
|
||||
|
||||
return new Response(null, {status: status, headers: {location: url}})
|
||||
};
|
||||
|
||||
exports.DOMException = global.DOMException;
|
||||
try {
|
||||
new exports.DOMException();
|
||||
} catch (err) {
|
||||
exports.DOMException = function(message, name) {
|
||||
this.message = message;
|
||||
this.name = name;
|
||||
var error = Error(message);
|
||||
this.stack = error.stack;
|
||||
};
|
||||
exports.DOMException.prototype = Object.create(Error.prototype);
|
||||
exports.DOMException.prototype.constructor = exports.DOMException;
|
||||
}
|
||||
|
||||
function fetch(input, init) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var request = new Request(input, init);
|
||||
|
||||
if (request.signal && request.signal.aborted) {
|
||||
return reject(new exports.DOMException('Aborted', 'AbortError'))
|
||||
}
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
|
||||
function abortXhr() {
|
||||
xhr.abort();
|
||||
}
|
||||
|
||||
xhr.onload = function() {
|
||||
var options = {
|
||||
status: xhr.status,
|
||||
statusText: xhr.statusText,
|
||||
headers: parseHeaders(xhr.getAllResponseHeaders() || '')
|
||||
};
|
||||
options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL');
|
||||
var body = 'response' in xhr ? xhr.response : xhr.responseText;
|
||||
setTimeout(function() {
|
||||
resolve(new Response(body, options));
|
||||
}, 0);
|
||||
};
|
||||
|
||||
xhr.onerror = function() {
|
||||
setTimeout(function() {
|
||||
reject(new TypeError('Network request failed'));
|
||||
}, 0);
|
||||
};
|
||||
|
||||
xhr.ontimeout = function() {
|
||||
setTimeout(function() {
|
||||
reject(new TypeError('Network request failed'));
|
||||
}, 0);
|
||||
};
|
||||
|
||||
xhr.onabort = function() {
|
||||
setTimeout(function() {
|
||||
reject(new exports.DOMException('Aborted', 'AbortError'));
|
||||
}, 0);
|
||||
};
|
||||
|
||||
function fixUrl(url) {
|
||||
try {
|
||||
return url === '' && global.location.href ? global.location.href : url
|
||||
} catch (e) {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
xhr.open(request.method, fixUrl(request.url), true);
|
||||
|
||||
if (request.credentials === 'include') {
|
||||
xhr.withCredentials = true;
|
||||
} else if (request.credentials === 'omit') {
|
||||
xhr.withCredentials = false;
|
||||
}
|
||||
|
||||
if ('responseType' in xhr) {
|
||||
if (support.blob) {
|
||||
xhr.responseType = 'blob';
|
||||
} else if (
|
||||
support.arrayBuffer &&
|
||||
request.headers.get('Content-Type') &&
|
||||
request.headers.get('Content-Type').indexOf('application/octet-stream') !== -1
|
||||
) {
|
||||
xhr.responseType = 'arraybuffer';
|
||||
}
|
||||
}
|
||||
|
||||
if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers)) {
|
||||
Object.getOwnPropertyNames(init.headers).forEach(function(name) {
|
||||
xhr.setRequestHeader(name, normalizeValue(init.headers[name]));
|
||||
});
|
||||
} else {
|
||||
request.headers.forEach(function(value, name) {
|
||||
xhr.setRequestHeader(name, value);
|
||||
});
|
||||
}
|
||||
|
||||
if (request.signal) {
|
||||
request.signal.addEventListener('abort', abortXhr);
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
// DONE (success or failure)
|
||||
if (xhr.readyState === 4) {
|
||||
request.signal.removeEventListener('abort', abortXhr);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit);
|
||||
})
|
||||
}
|
||||
|
||||
fetch.polyfill = true;
|
||||
|
||||
if (!global.fetch) {
|
||||
global.fetch = fetch;
|
||||
global.Headers = Headers;
|
||||
global.Request = Request;
|
||||
global.Response = Response;
|
||||
}
|
||||
|
||||
exports.Headers = Headers;
|
||||
exports.Request = Request;
|
||||
exports.Response = Response;
|
||||
exports.fetch = fetch;
|
||||
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
|
||||
})));
|
||||
6
assets/javascripts/popper.min.js
vendored
3
assets/javascripts/purify.min.js
vendored
1
assets/javascripts/url-polyfill.min.js
vendored
45
bin/feedtest.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/nkanaev/yarr/src/parser"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Println("usage: <script> [url|filepath]")
|
||||
return
|
||||
}
|
||||
url := os.Args[1]
|
||||
var r io.Reader
|
||||
|
||||
if strings.HasPrefix(url, "http") {
|
||||
res, err := http.Get(url)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get url %s: %s", url, err)
|
||||
}
|
||||
r = res.Body
|
||||
} else {
|
||||
var err error
|
||||
r, err = os.Open(url)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to open file: %s", err)
|
||||
}
|
||||
}
|
||||
feed, err := parser.Parse(r)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to parse feed: %s", err)
|
||||
}
|
||||
body, err := json.MarshalIndent(feed, "", " ")
|
||||
if err != nil {
|
||||
log.Fatalf("failed to marshall feed: %s", err)
|
||||
}
|
||||
fmt.Println(string(body))
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"flag"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -85,9 +85,9 @@ func main() {
|
||||
for _, res := range []int{1024, 512, 256, 128, 64, 32, 16} {
|
||||
outfile := fmt.Sprintf("icon_%dx%d.png", res, res)
|
||||
if res == 1024 || res == 64 {
|
||||
outfile = fmt.Sprintf("icon_%dx%d@2x.png", res / 2, res / 2)
|
||||
outfile = fmt.Sprintf("icon_%dx%d@2x.png", res/2, res/2)
|
||||
}
|
||||
cmd := []string {
|
||||
cmd := []string{
|
||||
"sips", "-s", "format", "png", "--resampleWidth", strconv.Itoa(res),
|
||||
iconFile, "--out", path.Join(iconsetDir, outfile),
|
||||
}
|
||||
41
bin/reader.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/nkanaev/yarr/src/content/readability"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Println("usage: <script> [url]")
|
||||
return
|
||||
}
|
||||
url := os.Args[1]
|
||||
var r io.Reader
|
||||
|
||||
if strings.HasPrefix(url, "http") {
|
||||
res, err := http.Get(url)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get url %s: %s", url, err)
|
||||
}
|
||||
r = res.Body
|
||||
} else {
|
||||
var err error
|
||||
r, err = os.Open(url)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to open file: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
content, err := readability.ExtractContent(r)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to extract content: %s", err)
|
||||
}
|
||||
fmt.Println(content)
|
||||
}
|
||||
44
build.md
Normal file
@@ -0,0 +1,44 @@
|
||||
## Compilation
|
||||
|
||||
Install `Go >= 1.17` and `GCC`. Get the source code:
|
||||
|
||||
git clone https://github.com/nkanaev/yarr.git
|
||||
|
||||
Then run one of the corresponding commands:
|
||||
|
||||
# create an executable for the host os
|
||||
make build_macos # -> _output/macos/yarr.app
|
||||
make build_linux # -> _output/linux/yarr
|
||||
make build_windows # -> _output/windows/yarr.exe
|
||||
|
||||
# host-specific cli version (no gui)
|
||||
make build_default # -> _output/yarr
|
||||
|
||||
# ... or start a dev server locally
|
||||
make serve # starts a server at http://localhost:7070
|
||||
|
||||
# ... or build a docker image
|
||||
docker build -t yarr .
|
||||
|
||||
## ARM compilation
|
||||
|
||||
The instructions below are to cross-compile *yarr* to `Linux/ARM*`.
|
||||
|
||||
Build:
|
||||
|
||||
docker build -t yarr.arm -f dockerfile.arm .
|
||||
|
||||
Test:
|
||||
|
||||
# inside host
|
||||
docker run -it --rm yarr.arm
|
||||
|
||||
# then, inside container
|
||||
cd /root/out
|
||||
qemu-aarch64 -L /usr/aarch64-linux-gnu/ yarr.arm64
|
||||
|
||||
Extract files from images:
|
||||
|
||||
CID=$(docker create yarr.arm)
|
||||
docker cp -a "$CID:/root/out" .
|
||||
docker rm "$CID"
|
||||
100
doc/changelog.txt
Normal file
@@ -0,0 +1,100 @@
|
||||
# upcoming
|
||||
|
||||
- (new) Fever API support (thanks to @icefed)
|
||||
- (fix) duplicate articles caused by the same feed addition (thanks to @adaszko)
|
||||
- (fix) relative article links (thanks to @adazsko for the report)
|
||||
- (fix) atom article links stored in id element (thanks to @adazsko for the report)
|
||||
|
||||
# v2.4 (2023-08-15)
|
||||
|
||||
- (new) ARM build support (thanks to @tillcash & @fenuks)
|
||||
- (new) auth configuration via param or env variable (thanks to @pierreprinetti)
|
||||
- (new) web app manifest for an app-like experience on mobile (thanks to @qbit)
|
||||
- (fix) concurrency issue crashing the app (thanks to @quoing)
|
||||
- (fix) favicon visibility in dark mode (thanks to @caycaycarly for the report)
|
||||
- (fix) autoloading more articles not working in certain edge cases (thanks to @fenuks for the report)
|
||||
- (fix) handle Google URL redirects in "Read Here" (thanks to @cubbei for discovery)
|
||||
- (fix) handle failures to extract content in "Read Here" (thanks to @grigio for the report)
|
||||
- (fix) article view width for high resolution screens (thanks to @whaler-ragweed for the report)
|
||||
- (fix) make newly added feed searchable (thanks to @BMorearty for the report)
|
||||
- (fix) feed/article selection accessibility via arrow keys (thanks to @grigio and @tillcash)
|
||||
- (fix) keyboard shortcuts in Firefox (thanks to @kaloyan13)
|
||||
- (fix) keyboard shortcuts in non-English layouts (thanks to @kaloyan13)
|
||||
- (fix) sorting articles with timezone information (thanks to @x2cf)
|
||||
- (fix) handling links set in guid only for certain feeds (thanks to @adaszko for the report)
|
||||
- (fix) crashes caused by feed icon endpoint (thanks to @adaszko)
|
||||
|
||||
# v2.3 (2022-05-03)
|
||||
|
||||
- (fix) handling encodings (thanks to @f100024 & @fserb)
|
||||
- (fix) parsing xml feeds with illegal characters (thanks to @stepelu for the report)
|
||||
- (fix) old articles reappearing as unread (thanks to @adaszko for the report)
|
||||
- (fix) item list scrolling issue on large screens (thanks to @bielej for the report)
|
||||
- (fix) keyboard shortcuts color in dark mode (thanks to @John09f9 for the report)
|
||||
- (etc) autofocus when adding a new feed (thanks to @lakuapik)
|
||||
|
||||
# v2.2 (2021-11-20)
|
||||
|
||||
- (fix) windows console support (thanks to @dufferzafar for the report)
|
||||
- (fix) remove html tags from article titles (thanks to Alex Went for the report)
|
||||
- (etc) autoselect current folder when adding a new feed (thanks to @krkk)
|
||||
- (etc) folder/feed settings menu available across all filters
|
||||
|
||||
# v2.1 (2021-08-16)
|
||||
|
||||
- (new) configuration via env variables
|
||||
- (fix) missing `content-type` headers (thanks to @verahawk for the report)
|
||||
- (fix) handle opml files not following the spec (thanks to @huangnauh for the report)
|
||||
- (fix) pagination in unread/starred feeds (thanks to @Farow for the report)
|
||||
- (fix) handling feeds with non-utf8 encodings (thanks to @fserb for the report)
|
||||
- (fix) errors caused by empty feeds (thanks to @decke)
|
||||
- (fix) recognize all audio mime types as podcasts (thanks to @krkk)
|
||||
- (fix) ui tweaks (thanks to @Farow)
|
||||
|
||||
# v2.0 (2021-04-18)
|
||||
|
||||
- (new) user interface tweaks
|
||||
- (new) feed parser fully rewritten
|
||||
- (new) show youtube/vimeo iframes in "read here"
|
||||
- (new) keyboard shortcuts for article scrolling & toggling "read here"
|
||||
- (new) more options for auto-refresh intervals
|
||||
- (fix) `-base` not serving static files (thanks to @vfaronov)
|
||||
- (etc) 3rd-party dependencies reduced to the bare minimum
|
||||
|
||||
special thanks to @tillcash for feedback & suggestions.
|
||||
|
||||
# v1.4 (2021-03-11)
|
||||
|
||||
- (new) keyboard shortcuts (thanks to @Duarte-Dias)
|
||||
- (new) show podcast audio
|
||||
- (fix) deleting feeds
|
||||
- (etc) minor ui tweaks & changes
|
||||
|
||||
# v1.3 (2021-02-18)
|
||||
|
||||
- (fix) log out functionality if authentication is set
|
||||
- (fix) import opml if authentication is set
|
||||
- (fix) login page if authentication is set (thanks to @einschmidt)
|
||||
|
||||
# v1.2 (2021-02-11)
|
||||
|
||||
- (new) autorefresh rate
|
||||
- (new) reduced bandwidth usage via stateful http headers `last-modified/etag`
|
||||
- (new) show feed errors in feed management modal
|
||||
- (new) `-open` flag for automatically opening the server url
|
||||
- (new) `-base` flag for serving urls under non-root path (thanks to @hcl)
|
||||
- (new) `-auth-file` flag for authentication
|
||||
- (new) `-cert-file` & `-key-file` flags for TLS
|
||||
- (fix) wrapping long words in the ui to prevent vertical scroll
|
||||
- (fix) increased toolbar height in mobile/tablet layout (thanks to @einschmidt)
|
||||
|
||||
# v1.1 (2020-10-05)
|
||||
|
||||
- (new) responsive design
|
||||
- (fix) server crash on favicon fetch timeout (reported by @minioin)
|
||||
- (fix) handling byte order marks in feeds (reported by @ilaer)
|
||||
- (fix) deleting a feed raises exception in the ui if the feed's items are shown.
|
||||
|
||||
# v1.0 (2020-09-24)
|
||||
|
||||
Initial Release
|
||||
171
doc/formats.txt
Normal file
@@ -0,0 +1,171 @@
|
||||
# model
|
||||
|
||||
- feed:
|
||||
- title
|
||||
|
||||
rdf>channel>title (rss 0.90)
|
||||
rdf>channel>title (rss 1.0)
|
||||
rss>channel>title (rss 0.91 netscape)
|
||||
rss>channel>title (rss 0.91 userland)
|
||||
rss>channel>title (rss 2.0)
|
||||
feed>title (atom 1.0)
|
||||
|
||||
- site_url
|
||||
|
||||
rdf>channel>link (rss 0.90)
|
||||
rdf>channel>link (rss 1.0)
|
||||
rss>channel>link (rss 0.91 netscape)
|
||||
rss>channel>link (rss 0.91 userland)
|
||||
rss>channel>link (rss 2.0)
|
||||
feed>link (atom 1.0)
|
||||
|
||||
- item:
|
||||
- guid
|
||||
|
||||
rss>channel>guid (rss 2.0)
|
||||
feed>entry>id (atom 1.0)
|
||||
|
||||
- date
|
||||
|
||||
rdf>item>dc:date (rss 1.0)
|
||||
rss>channel>pubDate (rss 2.0)
|
||||
feed>entry>updated (atom 1.0)
|
||||
feed>entry>published (atom 1.0)
|
||||
|
||||
- url
|
||||
|
||||
rdf>item>link (rss 0.90)
|
||||
rdf>item>link (rss 1.0)
|
||||
rss>channel>item>link (rss 0.91 netscape)
|
||||
rss>channel>item>link (rss 0.91 userland)
|
||||
rss>channel>item>link (rss 2.0)
|
||||
feed>entry>link[rel=alternate] (atom 1.0)
|
||||
|
||||
- title
|
||||
|
||||
rdf>item>title (rss 0.90)
|
||||
rdf>item>title (rss 1.0)
|
||||
rss>channel>item>title (rss 0.91 netscape)
|
||||
rss>channel>item>title (rss 0.91 userland)
|
||||
rss>channel>item>title (rss 2.0)
|
||||
feed>entry>title (atom 1.0)
|
||||
|
||||
- content
|
||||
|
||||
rss>channel>item>description (rss 0.91 netscape)
|
||||
rss>channel>item>description (rss 0.91 userland)
|
||||
rss>channel>item>description (rss 2.0)
|
||||
rdf>item>description (rss 1.0)
|
||||
rdf>item>content:encoded (rss 1.0)
|
||||
feed>entry>content (atom 1.0)
|
||||
|
||||
- image_url
|
||||
|
||||
rss>item>media:thumbnail:url (rss 2.0 media)
|
||||
feed>entry>enclosure[rel='image/*'] (atom 1.0) ???
|
||||
|
||||
- audio_url
|
||||
|
||||
rss>item>enclosure:url (audio/*) (rss 2.0)
|
||||
feed>entry>enclosure (audio/*') (atom 1.0) ???
|
||||
|
||||
# specs
|
||||
|
||||
- rss
|
||||
https://en.wikipedia.org/wiki/RSS
|
||||
- 0.90:
|
||||
https://www.rssboard.org/rss-0-9-0
|
||||
https://web.archive.org/web/20001208063100/http://my.netscape.com/publish/help/quickstart.html
|
||||
- 0.91 (netscape)
|
||||
https://www.rssboard.org/rss-0-9-1-netscape
|
||||
- 0.91 (userland)
|
||||
https://www.rssboard.org/rss-0-9-1
|
||||
- 0.92
|
||||
https://www.rssboard.org/rss-0-9-2
|
||||
by userland, no significant changes from 0.91
|
||||
- 0.93 (withdrawn)
|
||||
http://backend.userland.com/rss093
|
||||
- 0.94 (withdrawn)
|
||||
- 1.0
|
||||
https://web.resource.org/rss/1.0/
|
||||
https://web.archive.org/web/20021014094554/https://web.resource.org/rss/1.0/spec
|
||||
reintroduced rdf from 0.90, added dublincore namespaces etc
|
||||
namespaces:
|
||||
content: http://purl.org/rss/1.0/modules/content/
|
||||
dc: http://purl.org/dc/elements/1.1/
|
||||
- 2.0
|
||||
https://cyber.harvard.edu/rss/rss.html
|
||||
https://www.rssboard.org/rss-2-0
|
||||
|
||||
- atom
|
||||
https://en.wikipedia.org/wiki/Atom_(Web_standard)
|
||||
- 0.3
|
||||
https://support.google.com/merchants/answer/160598?hl=en
|
||||
http://web.archive.org/web/20060811235523/http://www.mnot.net/drafts/draft-nottingham-atom-format-02.html
|
||||
- 1.0
|
||||
https://tools.ietf.org/html/rfc4287
|
||||
https://validator.w3.org/feed/docs/atom.html
|
||||
|
||||
- json
|
||||
https://en.wikipedia.org/wiki/JSON_Feed
|
||||
- 1.0
|
||||
https://jsonfeed.org/version/1
|
||||
- 1.1
|
||||
https://jsonfeed.org/version/1.1
|
||||
|
||||
- media
|
||||
https://www.rssboard.org/media-rss
|
||||
xml namespace for:
|
||||
- rss 2.0
|
||||
- atom 1.0
|
||||
|
||||
# extensions
|
||||
|
||||
- feedburner
|
||||
https://en.wikipedia.org/wiki/FeedBurner
|
||||
|
||||
- media
|
||||
https://www.rssboard.org/media-rss
|
||||
initially for rss 2.0, used in atom 1.0 as well (youtube)
|
||||
|
||||
- itunes podcasts
|
||||
https://help.apple.com/itc/podcasts_connect/#/itcb54353390
|
||||
https://github.com/simplepie/simplepie-ng/wiki/Spec:-iTunes-Podcast-RSS
|
||||
|
||||
- google podcasts
|
||||
https://support.google.com/podcast-publishers/answer/9889544?visit_id=637523492443301715-1225759684&rd=1
|
||||
|
||||
# parsers
|
||||
|
||||
https://github.com/kurtmckee/feedparser
|
||||
https://github.com/mmcdole/gofeed
|
||||
https://github.com/miniflux/v2/tree/2.0.28/reader/
|
||||
https://github.com/Ranchero-Software/RSParser
|
||||
https://github.com/feederco/feeder-parser
|
||||
|
||||
https://github.com/mmcdole/gofeed/commit/9665eb31016cef3d15ab85574bc6fdbe890cd252
|
||||
|
||||
# platforms
|
||||
|
||||
A list of centralized content providers worth keeping track of.
|
||||
The parser should be reasonably handle content provided by them.
|
||||
Delete any from the list in case they drop support of web feeds.
|
||||
|
||||
- blogger
|
||||
- cnblogs
|
||||
- flickr
|
||||
- hatenablog
|
||||
- livejournal
|
||||
- medium
|
||||
- posthaven
|
||||
- reddit
|
||||
- substack
|
||||
- tumblr
|
||||
- vimeo
|
||||
- wordpress
|
||||
- youtube
|
||||
|
||||
# links
|
||||
|
||||
https://indieweb.org/feed#Criticism
|
||||
https://inessential.com/2013/03/18/brians_stupid_feed_tricks
|
||||
70
doc/platform.txt
Normal file
@@ -0,0 +1,70 @@
|
||||
Incomplete & inaccurate platform-specific notes.
|
||||
|
||||
# MacOS Icon
|
||||
|
||||
The format for desktop apps is [.icns][icns].
|
||||
AFAIK, the format is not open (even though it had been [reverse-engineered][icns-re]),
|
||||
and I couldn't find any 3rd party tool that'd fully support it.
|
||||
|
||||
The easiest way for creating icon file is either via `Xcode`,
|
||||
or by using built-in `iconutil` command that ships with MacOS.
|
||||
|
||||
The steps are provided below:
|
||||
|
||||
$ sips -s format png --resampleWidth 1024 source.png --out /path/to/icons/icon_512x512@2x.png
|
||||
$ sips -s format png --resampleWidth 512 source.png --out /path/to/icons/icon_512x512.png
|
||||
$ sips -s format png --resampleWidth 256 source.png --out /path/to/icons/icon_256x256.png
|
||||
$ sips -s format png --resampleWidth 128 source.png --out /path/to/icons/icon_128x128.png
|
||||
$ sips -s format png --resampleWidth 64 source.png --out /path/to/icons/icon_32x32@2x.png
|
||||
$ sips -s format png --resampleWidth 32 source.png --out /path/to/icons/icon_32x32.png
|
||||
$ sips -s format png --resampleWidth 16 source.png --out /path/to/icons/icon_16x16.png
|
||||
$ iconutil -c icns /path/to/icons -o icon.icns
|
||||
|
||||
[icns]: https://en.wikipedia.org/wiki/Apple_Icon_Image_format
|
||||
[icns-re]: https://www.macdisk.com/maciconen.php#RLE
|
||||
|
||||
# Windows Icon
|
||||
|
||||
Terminology:
|
||||
|
||||
- coff: precursor to pe format (portable executable). pe is an extension of coff.
|
||||
- manifest: xml file with platform requirements needed during runtime
|
||||
- https://docs.microsoft.com/en-us/windows/win32/sbscs/application-manifests
|
||||
- https://www.samlogic.net/articles/manifest.htm
|
||||
- rc: dsl file that describes the application metadata & resources
|
||||
- https://docs.microsoft.com/en-gb/windows/win32/menurc/about-resource-files
|
||||
- https://github.com/josephspurrier/goversioninfo/blob/master/testdata/rc/versioninfo.rc (sample rc)
|
||||
|
||||
Windows Icons are directly embedded to the binary.
|
||||
To do so one needs to provide `.syso` file prior to compiling Go code,
|
||||
which will be passed to the linker. So, basically `.syso` is any
|
||||
[object file][obj-file] that the linker understands.
|
||||
|
||||
More info here: [ticket][syso-ticket] & [commit][syso-commit].
|
||||
|
||||
Note to self: running `go build main.go` [won't embed][syso-quirk]
|
||||
.syso file if it isn't located in a package directory.
|
||||
|
||||
Tools to create `.syso` files:
|
||||
|
||||
- [windres][windres]: ships with mingw (gnu tools for windows)
|
||||
- [rsrc][rsrc]: written in Go, wasn't considered at the time
|
||||
due to the critical bug with icon alignment
|
||||
- [goversioninfo][goversioninfo]: rsrc wrapper
|
||||
with manifest file creation via json
|
||||
|
||||
[obj-file]: https://en.wikipedia.org/wiki/Object_file
|
||||
[syso-linker]: https://github.com/golang/go/issues/23278#issuecomment-354567634
|
||||
[syso-ticket]: https://github.com/golang/go/issues/1552
|
||||
[syso-commit]: https://github.com/golang/go/commit/b0996334
|
||||
[syso-quirk]: https://github.com/golang/go/issues/16090
|
||||
[mingw]: https://en.wikipedia.org/wiki/MinGW
|
||||
[coff]: https://en.wikipedia.org/wiki/COFF
|
||||
[windres]: https://sourceware.org/binutils/docs/binutils/windres.html
|
||||
[rsrs]: https://github.com/akavel/rsrc
|
||||
[rsrc-bug]: https://github.com/akavel/rsrc/issues/12
|
||||
[goversioninfo]: github.com/josephspurrier/goversioninfo
|
||||
|
||||
[winicon-guide]: https://docs.microsoft.com/en-us/windows/win32/uxguide/vis-icons#size-requirements
|
||||
[res-vs-coff]: http://www.mingw.org/wiki/MS_resource_compiler
|
||||
[versioninfo-resource]: https://docs.microsoft.com/en-us/windows/win32/menurc/versioninfo-resource
|
||||
62
doc/rationale.txt
Normal file
@@ -0,0 +1,62 @@
|
||||
# goal
|
||||
|
||||
The goal is to ship a (subjectively ergonomic) software for reading feeds.
|
||||
*yarr* is not designed/intended to be used as an archiving tool.
|
||||
|
||||
The initial goal was to serve the author's needs:
|
||||
a desktop application accessible via web browser
|
||||
(because keeping 2 apps running, feed reader & browser, was annoying).
|
||||
|
||||
# interface
|
||||
|
||||
The UI aesthetics were inspired by the works of:
|
||||
|
||||
- Antoine Plu
|
||||
https://dribbble.com/antoineplu
|
||||
- Pawel Kuna
|
||||
https://github.com/codecalm
|
||||
https://github.com/tabler/tabler
|
||||
- Pawel Kadysz
|
||||
https://dribbble.com/pawelkadysz
|
||||
- Palantir
|
||||
https://github.com/palantir/blueprint
|
||||
- Yan Zhu
|
||||
https://github.com/picturepan2/spectre
|
||||
|
||||
The 3-column layout (feeds + items + read) & certain UI/navigation
|
||||
elements were based on & largely inspired by `Reeder 3`, `NetNewsWire` & `Feedbin`.
|
||||
|
||||
Alternative layouts *might* be introduced in the future, but are not guaranteed.
|
||||
|
||||
Ideas for 1-column layout:
|
||||
- stringer
|
||||
https://github.com/swanson/stringer
|
||||
- headline
|
||||
https://github.com/zserge/headline
|
||||
- miniflux
|
||||
https://miniflux.app/
|
||||
|
||||
Ideas for 2-column layout:
|
||||
- feedly
|
||||
https://feedly.com/
|
||||
- vienna (classic `|-` shaped layout)
|
||||
https://github.com/ViennaRSS/vienna-rss
|
||||
|
||||
# frontend
|
||||
|
||||
ES5 is preferred over ES6 until js transpilers (babeljs)
|
||||
become a thing of the past.
|
||||
|
||||
The project won't introduce node/npm ecosystem,
|
||||
3rd party js code is directly included into the project.
|
||||
|
||||
# backend
|
||||
|
||||
The reasons for Go:
|
||||
- single binary compilation
|
||||
- availability of 3rd party libraries
|
||||
- the author's excuse to learn go
|
||||
|
||||
The reasons for SQLite:
|
||||
- lack of need for db setup (huge plus for desktop)
|
||||
- SQL is boring & practical
|
||||
27
doc/thirdparty.txt
Normal file
@@ -0,0 +1,27 @@
|
||||
Below is the list of 3-rd party code directly included & rewritten
|
||||
to suit the author's needs & preferences.
|
||||
The licenses are included, and the authorship comments are left intact.
|
||||
|
||||
- readability
|
||||
https://github.com/miniflux/v2 (commit:31435ef) Apache 2.0
|
||||
|
||||
removed goquery dependency
|
||||
removed assisting utility structs (need another way to debug node scores)
|
||||
|
||||
- sanitizer
|
||||
https://github.com/miniflux/v2 (commit:3cb04b2) Apache 2.0
|
||||
|
||||
whitelist changed to the ones from https://github.com/cure53/DOMPurify:
|
||||
- allowed tags
|
||||
- allowed uri schemes
|
||||
- added svg whitelist
|
||||
|
||||
- systray
|
||||
https://github.com/getlantern/systray (commit:2c0986d) Apache 2.0
|
||||
|
||||
removed golog dependency
|
||||
|
||||
- fixconsole
|
||||
https://github.com/apenwarr/fixconsole (commit:5a9f648) Apache 2.0
|
||||
|
||||
removed `w32` dependency
|
||||
0
doc/todo.txt
Normal file
13
dockerfile
@@ -1,9 +1,12 @@
|
||||
FROM golang:1.15 AS build
|
||||
RUN apt install gcc -y
|
||||
FROM golang:alpine AS build
|
||||
RUN apk add build-base git
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN make build_linux
|
||||
|
||||
FROM ubuntu:20.04
|
||||
COPY --from=build /src/_output/linux/yarr /usr/bin/yarr
|
||||
ENTRYPOINT ["/usr/bin/yarr", "-addr", "0.0.0.0:7070"]
|
||||
FROM alpine:latest
|
||||
RUN apk add --no-cache ca-certificates && \
|
||||
update-ca-certificates
|
||||
COPY --from=build /src/_output/linux/yarr /usr/local/bin/yarr
|
||||
EXPOSE 7070
|
||||
CMD ["/usr/local/bin/yarr", "-addr", "0.0.0.0:7070", "-db", "/data/yarr.db"]
|
||||
|
||||
44
dockerfile.arm
Normal file
@@ -0,0 +1,44 @@
|
||||
FROM ubuntu:20.04
|
||||
|
||||
# Install GCC
|
||||
RUN apt update
|
||||
RUN apt install -y \
|
||||
wget build-essential \
|
||||
gcc-aarch64-linux-gnu \
|
||||
binutils-aarch64-linux-gnu binutils-aarch64-linux-gnu-dbg \
|
||||
gcc-arm-linux-gnueabihf \
|
||||
binutils-arm-linux-gnueabihf binutils-arm-linux-gnueabihf-dbg
|
||||
RUN env DEBIAN_FRONTEND=noninteractive \
|
||||
apt install -y qemu-user qemu-user-static
|
||||
|
||||
# Install Golang
|
||||
RUN wget --quiet https://go.dev/dl/go1.18.2.linux-amd64.tar.gz && \
|
||||
rm -rf /usr/local/go && \
|
||||
tar -C /usr/local -xzf go1.18.2.linux-amd64.tar.gz
|
||||
ENV PATH=$PATH:/usr/local/go/bin
|
||||
|
||||
# Copy source code
|
||||
WORKDIR /root/src
|
||||
RUN mkdir /root/out
|
||||
COPY . .
|
||||
|
||||
# Build ARM64
|
||||
RUN env \
|
||||
CC=aarch64-linux-gnu-gcc \
|
||||
CGO_ENABLED=1 \
|
||||
GOOS=linux GOARCH=arm64 \
|
||||
go build \
|
||||
-tags "sqlite_foreign_keys release linux" \
|
||||
-ldflags="-s -w" \
|
||||
-o /root/out/yarr.arm64 src/main.go
|
||||
|
||||
RUN env \
|
||||
CC=arm-linux-gnueabihf-gcc \
|
||||
CGO_ENABLED=1 \
|
||||
GOOS=linux GOARCH=arm GOARM=7 \
|
||||
go build \
|
||||
-tags "sqlite_foreign_keys release linux" \
|
||||
-ldflags="-s -w" \
|
||||
-o /root/out/yarr.arm7 src/main.go
|
||||
|
||||
CMD ["/bin/bash"]
|
||||
23
etc/install-linux.sh
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
|
||||
cat >"$HOME/.local/share/applications/yarr.desktop" <<END
|
||||
[Desktop Entry]
|
||||
Name=yarr
|
||||
Exec=$HOME/.local/bin/yarr -open
|
||||
Icon=yarr
|
||||
Type=Application
|
||||
Categories=Internet;
|
||||
END
|
||||
|
||||
cat >"$HOME/.local/share/icons/yarr.svg" <<END
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-anchor-favicon">
|
||||
<circle cx="12" cy="5" r="3" stroke-width="4" stroke="#ffffff"></circle>
|
||||
<line x1="12" y1="22" x2="12" y2="8" stroke-width="4" stroke="#ffffff"></line>
|
||||
<path d="M5 12H2a10 10 0 0 0 20 0h-3" stroke-width="4" stroke="#ffffff"></path>
|
||||
|
||||
<circle cx="12" cy="5" r="3"></circle>
|
||||
<line x1="12" y1="22" x2="12" y2="8"></line>
|
||||
<path d="M5 12H2a10 10 0 0 0 20 0h-3"></path>
|
||||
</svg>
|
||||
END
|
||||
BIN
etc/promo.png
Normal file
|
After Width: | Height: | Size: 173 KiB |
19
fever.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Fever API support
|
||||
|
||||
Fever API is a kind of RSS HTTP API interface, because the Fever API definition is not very clear, so the implementation of Fever server and Client may have some compatibility problems.
|
||||
|
||||
The Fever API implemented by Yarr is based on the Fever API spec: https://github.com/DigitalDJ/tinytinyrss-fever-plugin/blob/master/fever-api.md.
|
||||
|
||||
Here are some Apps that have been tested to work with yarr. Feel free to test other Clients/Apps and update the list here.
|
||||
|
||||
> Different apps support different URL/Address formats. Please note whether the URL entered has `http://` scheme and `/` suffix.
|
||||
|
||||
| App | Platforms | Config Server URL |
|
||||
|:------------------------------------------------------------------------- | ---------------- |:--------------------------------------------------- |
|
||||
| [Reeder](https://reederapp.com/) | MacOS<br>iOS | 127.0.0.1:7070/fever<br>http://127.0.0.1:7070/fever |
|
||||
| [ReadKit](https://readkit.app/) | MacOS<br>iOS | http://127.0.0.1:7070/fever |
|
||||
| [Fluent Reader](https://github.com/yang991178/fluent-reader) | MacOS<br>Windows | http://127.0.0.1:7070/fever/ |
|
||||
| [Unread](https://apps.apple.com/us/app/unread-an-rss-reader/id1363637349) | iOS | http://127.0.0.1:7070/fever |
|
||||
| [Fiery Feeds](https://voidstern.net/fiery-feeds) | MacOS<br>iOS | http://127.0.0.1:7070/fever |
|
||||
|
||||
If you are having trouble using Fever, please open an issue and @icefed, thanks.
|
||||
13
go.mod
@@ -1,14 +1,11 @@
|
||||
module github.com/nkanaev/yarr
|
||||
|
||||
go 1.14
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.5.1
|
||||
github.com/getlantern/systray v1.0.4
|
||||
github.com/mattn/go-sqlite3 v1.14.0
|
||||
github.com/mmcdole/gofeed v1.0.0
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e
|
||||
github.com/mattn/go-sqlite3 v1.14.7
|
||||
golang.org/x/net v0.8.0
|
||||
golang.org/x/sys v0.6.0
|
||||
)
|
||||
|
||||
replace github.com/mmcdole/gofeed => ./gofeed
|
||||
require golang.org/x/text v0.8.0 // indirect
|
||||
|
||||
104
go.sum
@@ -1,73 +1,39 @@
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
|
||||
github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE=
|
||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkbQ3slBdOA=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
|
||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
|
||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
|
||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
|
||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
|
||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
||||
github.com/getlantern/systray v1.0.4 h1:qJ/bOlYhn5nsj2FejutWWVFMbhOkYhsChoy26OjgZgU=
|
||||
github.com/getlantern/systray v1.0.4/go.mod h1:AecygODWIsBquJCJFop8MEQcJbWFfw/1yWbVabNgpCM=
|
||||
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
|
||||
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||
github.com/mmcdole/gofeed v1.0.0 h1:PHqwr8fsEm8xarj9s53XeEAFYhRM3E9Ib7Ie766/LTE=
|
||||
github.com/mmcdole/gofeed v1.0.0/go.mod h1:tkVcyzS3qVMlQrQxJoEH1hkTiuo9a8emDzkMi7TZBu0=
|
||||
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf h1:sWGE2v+hO0Nd4yFU/S/mDBM5plIU8v/Qhfz41hkDIAI=
|
||||
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf/go.mod h1:pasqhqstspkosTneA62Nc+2p9SOBBYAPbnmRRWPQ0V8=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/mattn/go-sqlite3 v1.14.7 h1:fxWBnXkxfM6sRiuH3bqJ4CfzZojMOLVc0UTsTglEghA=
|
||||
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9 h1:YTzHMGlqJu67/uEo1lBv0n3wBXhXNeUbB1XfN2vmTm0=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
37
hacking.md
@@ -1,37 +0,0 @@
|
||||
# hacking
|
||||
|
||||
If you have any questions/suggestions/proposals,
|
||||
you can reach out the author via e-mail (nkanaev@live.com)
|
||||
or mastodon (https://fosstodon.org/@nkanaev).
|
||||
|
||||
## build
|
||||
|
||||
Install `Go >= 1.14` and `gcc`. Get the source code:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/nkanaev/yarr.git
|
||||
git clone https://github.com/nkanaev/gofeed.git
|
||||
mv gofeed yarr
|
||||
cd yarr
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```sh
|
||||
# create a binary for the host os
|
||||
make build_macos # -> _output/macos/yarr.app
|
||||
make build_linux # -> _output/linux/yarr
|
||||
make build_windows # -> _output/windows/yarr.exe
|
||||
|
||||
# ... or run locally (for testing & hacking)
|
||||
go run main.go # starts a server at http://localhost:7070
|
||||
```
|
||||
|
||||
## plans
|
||||
|
||||
- feeds health checker
|
||||
- Fever API support
|
||||
|
||||
## code of conduct
|
||||
|
||||
Be excellent to each other. Party on, dudes!
|
||||
53
main.go
@@ -1,53 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/nkanaev/yarr/server"
|
||||
"github.com/nkanaev/yarr/storage"
|
||||
"github.com/nkanaev/yarr/platform"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var Version string = "0.0"
|
||||
var GitHash string = "unknown"
|
||||
|
||||
func main() {
|
||||
var addr, storageFile string
|
||||
var ver bool
|
||||
flag.StringVar(&addr, "addr", "127.0.0.1:7070", "address to run server on")
|
||||
flag.StringVar(&storageFile, "db", "", "storage file path")
|
||||
flag.BoolVar(&ver, "version", false, "print application version")
|
||||
flag.Parse()
|
||||
|
||||
if ver {
|
||||
fmt.Printf("v%s (%s)\n", Version, GitHash)
|
||||
return
|
||||
}
|
||||
|
||||
logger := log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile)
|
||||
|
||||
configPath, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
logger.Fatal("Failed to get config dir: ", err)
|
||||
}
|
||||
|
||||
if storageFile == "" {
|
||||
storagePath := filepath.Join(configPath, "yarr")
|
||||
if err := os.MkdirAll(storagePath, 0755); err != nil {
|
||||
logger.Fatal("Failed to create app config dir: ", err)
|
||||
}
|
||||
storageFile = filepath.Join(storagePath, "storage.db")
|
||||
}
|
||||
|
||||
db, err := storage.New(storageFile, logger)
|
||||
if err != nil {
|
||||
logger.Fatal("Failed to initialise database: ", err)
|
||||
}
|
||||
|
||||
srv := server.New(db, logger, addr)
|
||||
logger.Printf("starting server at http://%s", addr)
|
||||
platform.Start(srv)
|
||||
}
|
||||
46
makefile
@@ -1,41 +1,33 @@
|
||||
VERSION=1.1
|
||||
VERSION=2.4
|
||||
GITHASH=$(shell git rev-parse --short=8 HEAD)
|
||||
|
||||
ASSETS = assets/javascripts/* assets/stylesheets/* assets/graphicarts/* assets/index.html
|
||||
CGO_ENABLED=1
|
||||
|
||||
GO_LDFLAGS = -s -w
|
||||
GO_LDFLAGS := $(GO_LDFLAGS) -X 'main.Version=$(VERSION)' -X 'main.GitHash=$(GITHASH)'
|
||||
|
||||
default: bundle
|
||||
|
||||
server/assets.go: $(ASSETS)
|
||||
go run scripts/bundle_assets.go >/dev/null
|
||||
|
||||
bundle: server/assets.go
|
||||
|
||||
build_default: bundle
|
||||
build_default:
|
||||
mkdir -p _output
|
||||
go build -tags "sqlite_foreign_keys release" -ldflags="$(GO_LDFLAGS)" -o _output/yarr main.go
|
||||
go build -tags "sqlite_foreign_keys release" -ldflags="$(GO_LDFLAGS)" -o _output/yarr src/main.go
|
||||
|
||||
build_macos: bundle
|
||||
set GOOS=darwin
|
||||
set GOARCH=amd64
|
||||
build_macos:
|
||||
mkdir -p _output/macos
|
||||
go build -tags "sqlite_foreign_keys release macos" -ldflags="$(GO_LDFLAGS)" -o _output/macos/yarr main.go
|
||||
cp artwork/icon.png _output/macos/icon.png
|
||||
go run scripts/package_macos.go -outdir _output/macos -version "$(VERSION)"
|
||||
GOOS=darwin GOARCH=amd64 go build -tags "sqlite_foreign_keys release macos" -ldflags="$(GO_LDFLAGS)" -o _output/macos/yarr src/main.go
|
||||
cp src/platform/icon.png _output/macos/icon.png
|
||||
go run bin/package_macos.go -outdir _output/macos -version "$(VERSION)"
|
||||
|
||||
build_linux: bundle
|
||||
set GOOS=linux
|
||||
set GOARCH=386
|
||||
build_linux:
|
||||
mkdir -p _output/linux
|
||||
go build -tags "sqlite_foreign_keys release linux" -ldflags="$(GO_LDFLAGS)" -o _output/linux/yarr main.go
|
||||
GOOS=linux GOARCH=amd64 go build -tags "sqlite_foreign_keys release linux" -ldflags="$(GO_LDFLAGS)" -o _output/linux/yarr src/main.go
|
||||
|
||||
build_windows: bundle
|
||||
set GOOS=windows
|
||||
set GOARCH=386
|
||||
build_windows:
|
||||
mkdir -p _output/windows
|
||||
go run scripts/generate_versioninfo.go -version "$(VERSION)" -outfile artwork/versioninfo.rc
|
||||
windres -i artwork/versioninfo.rc -O coff -o platform/versioninfo.syso
|
||||
go build -tags "sqlite_foreign_keys release windows" -ldflags="$(GO_LDFLAGS) -H windowsgui" -o _output/windows/yarr.exe main.go
|
||||
go run bin/generate_versioninfo.go -version "$(VERSION)" -outfile src/platform/versioninfo.rc
|
||||
windres -i src/platform/versioninfo.rc -O coff -o src/platform/versioninfo.syso
|
||||
GOOS=windows GOARCH=amd64 go build -tags "sqlite_foreign_keys release windows" -ldflags="$(GO_LDFLAGS) -H windowsgui" -o _output/windows/yarr.exe src/main.go
|
||||
|
||||
serve:
|
||||
go run -tags "sqlite_foreign_keys" src/main.go -db local.db
|
||||
|
||||
test:
|
||||
cd src && go test -tags "sqlite_foreign_keys release" ./...
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
// +build !windows,!macos
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"github.com/nkanaev/yarr/server"
|
||||
)
|
||||
|
||||
func Start(s *server.Handler) {
|
||||
s.Start()
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
// +build macos
|
||||
|
||||
// File generated by 2goarray v0.1.0 (http://github.com/cratonica/2goarray)
|
||||
|
||||
package platform
|
||||
|
||||
var Icon []byte = []byte{
|
||||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
|
||||
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00, 0x45,
|
||||
0x08, 0x06, 0x00, 0x00, 0x00, 0x1c, 0x8d, 0x2b, 0x29, 0x00, 0x00, 0x00,
|
||||
0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x2e, 0x23, 0x00, 0x00, 0x2e,
|
||||
0x23, 0x01, 0x78, 0xa5, 0x3f, 0x76, 0x00, 0x00, 0x00, 0x19, 0x74, 0x45,
|
||||
0x58, 0x74, 0x53, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x00, 0x77,
|
||||
0x77, 0x77, 0x2e, 0x69, 0x6e, 0x6b, 0x73, 0x63, 0x61, 0x70, 0x65, 0x2e,
|
||||
0x6f, 0x72, 0x67, 0x9b, 0xee, 0x3c, 0x1a, 0x00, 0x00, 0x08, 0x55, 0x49,
|
||||
0x44, 0x41, 0x54, 0x78, 0x9c, 0xed, 0x9c, 0x5d, 0x6c, 0x1c, 0x57, 0x15,
|
||||
0xc7, 0xff, 0x67, 0x76, 0xd6, 0x93, 0xc4, 0x4e, 0xd6, 0x4d, 0x9b, 0x88,
|
||||
0x8f, 0x38, 0xa1, 0x84, 0x7e, 0xe1, 0xc4, 0x08, 0xb6, 0xd8, 0xbe, 0x77,
|
||||
0x97, 0xb0, 0x6d, 0xd3, 0x22, 0x2b, 0xa0, 0x16, 0x42, 0x1d, 0xf5, 0x21,
|
||||
0xa8, 0x25, 0x2f, 0x85, 0x92, 0x87, 0x20, 0x54, 0x5e, 0x90, 0x50, 0x85,
|
||||
0x10, 0x1f, 0xad, 0xa0, 0xa4, 0x91, 0x22, 0x54, 0xa0, 0x5f, 0x2f, 0x4d,
|
||||
0x53, 0x19, 0x22, 0x54, 0xb9, 0x24, 0x4a, 0xb2, 0x75, 0x3c, 0x77, 0x64,
|
||||
0x8c, 0x23, 0x30, 0x98, 0xd4, 0x11, 0x0d, 0x42, 0x76, 0x2b, 0x25, 0x51,
|
||||
0xdd, 0x6e, 0x9c, 0x34, 0xde, 0x31, 0xde, 0xc3, 0xc3, 0xce, 0x46, 0x8e,
|
||||
0x3d, 0x77, 0xf6, 0x6b, 0x66, 0x9d, 0xa2, 0xfc, 0x5e, 0x1c, 0xcf, 0x3d,
|
||||
0xf7, 0xcc, 0x7f, 0x4e, 0xe6, 0xe3, 0xde, 0x73, 0xcf, 0x35, 0x61, 0x09,
|
||||
0xc8, 0x64, 0x32, 0xcb, 0x5c, 0xd7, 0xbd, 0x07, 0xc0, 0xfd, 0x00, 0x3e,
|
||||
0x07, 0xe0, 0x63, 0x00, 0x6e, 0x04, 0xf0, 0x2e, 0x80, 0x77, 0x00, 0x9c,
|
||||
0x04, 0x70, 0xa8, 0xa9, 0xa9, 0xe9, 0x68, 0x36, 0x9b, 0x9d, 0x69, 0xb4,
|
||||
0x3e, 0x6a, 0xe4, 0xc9, 0x92, 0xc9, 0x64, 0xdc, 0xb2, 0xac, 0x47, 0x88,
|
||||
0xe8, 0x09, 0x66, 0xfe, 0x48, 0x05, 0x5d, 0xde, 0x05, 0xf0, 0x64, 0x22,
|
||||
0x91, 0x78, 0xba, 0xbf, 0xbf, 0x3f, 0x1f, 0xb5, 0xbe, 0x12, 0x0d, 0x0b,
|
||||
0x4a, 0x57, 0x57, 0xd7, 0x2d, 0xb1, 0x58, 0xec, 0x10, 0x80, 0x3b, 0x6a,
|
||||
0xe8, 0x3e, 0xc6, 0xcc, 0x0f, 0x38, 0x8e, 0xf3, 0xaf, 0xb0, 0x75, 0xf9,
|
||||
0xd1, 0x90, 0xa0, 0x08, 0x21, 0xbe, 0x48, 0x44, 0x7d, 0x00, 0x56, 0xd7,
|
||||
0xe1, 0x66, 0x8a, 0x99, 0xbf, 0xe6, 0x38, 0xce, 0x1b, 0x61, 0xe9, 0xd2,
|
||||
0x11, 0x79, 0x50, 0x84, 0x10, 0xb7, 0x13, 0x91, 0x03, 0xa0, 0x35, 0x04,
|
||||
0x77, 0x17, 0x88, 0x48, 0xda, 0xb6, 0x3d, 0x16, 0x82, 0x2f, 0x2d, 0x46,
|
||||
0x94, 0xce, 0x3b, 0x3a, 0x3a, 0x9a, 0x89, 0xe8, 0x8f, 0x08, 0x27, 0x20,
|
||||
0x00, 0xb0, 0x8a, 0x99, 0xfb, 0x92, 0xc9, 0xe4, 0x8a, 0x90, 0xfc, 0xf9,
|
||||
0x62, 0x46, 0xe9, 0xbc, 0xb9, 0xb9, 0xf9, 0xbb, 0x00, 0x3e, 0x55, 0xc6,
|
||||
0x6c, 0x02, 0x40, 0x16, 0xc0, 0x79, 0x00, 0x6b, 0x01, 0x64, 0x00, 0xac,
|
||||
0x0b, 0xb0, 0xbf, 0xd5, 0xb2, 0xac, 0x3d, 0x00, 0x7e, 0x1c, 0x82, 0x44,
|
||||
0x5f, 0x22, 0x7b, 0x7c, 0x3a, 0x3b, 0x3b, 0x6f, 0x34, 0x4d, 0xf3, 0x0c,
|
||||
0x80, 0x55, 0x1a, 0x93, 0xf3, 0x44, 0xb4, 0xdb, 0xb6, 0xed, 0x57, 0x00,
|
||||
0xf0, 0x7c, 0x4d, 0x52, 0xca, 0x1d, 0x00, 0x9e, 0x01, 0x70, 0x93, 0xa6,
|
||||
0xef, 0x05, 0x66, 0xbe, 0xd9, 0x71, 0x9c, 0xa9, 0x10, 0x25, 0x5f, 0x21,
|
||||
0xb2, 0xc7, 0x27, 0x1e, 0x8f, 0x7f, 0x15, 0xfa, 0x80, 0x9c, 0x05, 0x20,
|
||||
0x6c, 0xdb, 0x3e, 0x80, 0xab, 0x03, 0x02, 0x00, 0xac, 0x94, 0x7a, 0x19,
|
||||
0x40, 0x37, 0x80, 0x73, 0x9a, 0xfe, 0xab, 0x88, 0xe8, 0xfe, 0x70, 0x94,
|
||||
0x2e, 0x26, 0xb2, 0xa0, 0x30, 0xb3, 0x56, 0x34, 0x33, 0x3f, 0xac, 0x94,
|
||||
0x7a, 0x2b, 0xa8, 0xbf, 0xd7, 0xfe, 0xcd, 0x00, 0x93, 0x07, 0x6a, 0xd5,
|
||||
0x56, 0x8e, 0x28, 0x5f, 0xb4, 0xdd, 0x9a, 0xe3, 0x27, 0x1d, 0xc7, 0x79,
|
||||
0xbd, 0x12, 0x07, 0x4a, 0xa9, 0xd7, 0x00, 0xfc, 0xb5, 0x4a, 0xff, 0x75,
|
||||
0x13, 0x49, 0x50, 0x7a, 0x7a, 0x7a, 0x2c, 0x14, 0x87, 0xed, 0x7e, 0x54,
|
||||
0x14, 0x90, 0x12, 0x44, 0xa4, 0xb3, 0x5f, 0xd3, 0xde, 0xde, 0xde, 0x54,
|
||||
0x95, 0xb0, 0x0a, 0x89, 0x24, 0x28, 0x53, 0x53, 0x53, 0x6b, 0xa0, 0x79,
|
||||
0x89, 0x33, 0xf3, 0x3b, 0x55, 0xba, 0x7b, 0x5b, 0x73, 0x9c, 0x5a, 0x5a,
|
||||
0x5a, 0xd6, 0x56, 0xe9, 0xab, 0x22, 0x22, 0x09, 0x8a, 0x69, 0x9a, 0x39,
|
||||
0x5d, 0x1b, 0x11, 0x55, 0x35, 0x66, 0x61, 0x66, 0xad, 0xfd, 0xf2, 0xe5,
|
||||
0xcb, 0xdf, 0xaf, 0xc6, 0x57, 0xa5, 0x44, 0x12, 0x14, 0xdb, 0xb6, 0xa7,
|
||||
0x01, 0x4c, 0xfb, 0xb5, 0x31, 0x73, 0xaa, 0x4a, 0x77, 0x5f, 0xd0, 0x1c,
|
||||
0xcf, 0x65, 0xb3, 0xd9, 0x8b, 0x55, 0xfa, 0xaa, 0x88, 0x28, 0x5f, 0xb4,
|
||||
0xbe, 0x43, 0x71, 0x22, 0xba, 0x37, 0x95, 0x4a, 0xdd, 0x56, 0x89, 0x83,
|
||||
0xee, 0xee, 0xee, 0x3b, 0x00, 0xdc, 0x5d, 0x8d, 0xff, 0x30, 0x88, 0xf2,
|
||||
0x93, 0x7c, 0x48, 0xd3, 0x64, 0x32, 0xf3, 0x73, 0x99, 0x4c, 0x66, 0x59,
|
||||
0x50, 0xff, 0x4c, 0x26, 0xb3, 0x8c, 0x88, 0x9e, 0x83, 0x7e, 0xd4, 0xad,
|
||||
0xf3, 0x5f, 0x37, 0x91, 0x05, 0x25, 0x16, 0x8b, 0xf5, 0x01, 0x28, 0x68,
|
||||
0x9a, 0x85, 0xeb, 0xba, 0x87, 0xb7, 0x6c, 0xd9, 0xd2, 0xe6, 0xd7, 0x98,
|
||||
0x4e, 0xa7, 0xd7, 0xbb, 0xae, 0x7b, 0x84, 0x88, 0xba, 0x34, 0xfd, 0xe7,
|
||||
0x98, 0xb9, 0x2f, 0x14, 0xa1, 0x3e, 0x44, 0x3a, 0x4b, 0x4e, 0xa5, 0x52,
|
||||
0x2f, 0x30, 0xf3, 0x37, 0x02, 0x4c, 0x2e, 0x33, 0xf3, 0x2b, 0x44, 0x34,
|
||||
0x02, 0x60, 0x39, 0x80, 0xcb, 0x44, 0x74, 0x27, 0x33, 0xf7, 0x02, 0x08,
|
||||
0xba, 0x93, 0x9e, 0x57, 0x4a, 0x3d, 0x12, 0xaa, 0xd8, 0x79, 0x44, 0x1a,
|
||||
0x14, 0x29, 0xe5, 0x06, 0x66, 0x1e, 0x23, 0xa2, 0xe6, 0x10, 0xdd, 0x5e,
|
||||
0x34, 0x4d, 0xf3, 0xd3, 0x03, 0x03, 0x03, 0x13, 0x21, 0xfa, 0xbc, 0x8a,
|
||||
0x48, 0x53, 0x07, 0x4a, 0xa9, 0xff, 0x00, 0xd8, 0x09, 0xfd, 0x63, 0x54,
|
||||
0x2d, 0x4c, 0x44, 0xbb, 0xa2, 0x0c, 0x08, 0x00, 0xc4, 0xa2, 0x74, 0x0e,
|
||||
0x00, 0x93, 0x93, 0x93, 0x6f, 0xb6, 0xb5, 0xb5, 0xcd, 0x00, 0xd8, 0x8a,
|
||||
0xfa, 0xee, 0x4c, 0x26, 0xa2, 0xef, 0xdb, 0xb6, 0xfd, 0x6c, 0x48, 0xd2,
|
||||
0xb4, 0x44, 0x1e, 0x14, 0x00, 0x98, 0x98, 0x98, 0xb0, 0xd7, 0xaf, 0x5f,
|
||||
0x7f, 0x0a, 0xc0, 0x36, 0x00, 0xf1, 0x1a, 0x5c, 0xcc, 0x00, 0xd8, 0xa5,
|
||||
0x94, 0xda, 0x1f, 0xae, 0x32, 0x7f, 0x1a, 0x12, 0x14, 0x00, 0x98, 0x98,
|
||||
0x98, 0x18, 0x5b, 0xb7, 0x6e, 0x5d, 0x1f, 0x11, 0xb5, 0x01, 0xb8, 0xbd,
|
||||
0x8a, 0xae, 0x7f, 0x20, 0xa2, 0xaf, 0x2b, 0xa5, 0x8e, 0x45, 0xa5, 0x6d,
|
||||
0x21, 0x0d, 0x5d, 0xe2, 0x28, 0x21, 0x84, 0xe8, 0x34, 0x0c, 0xe3, 0x21,
|
||||
0x66, 0x7e, 0x14, 0xfe, 0x5f, 0x99, 0x19, 0x66, 0xde, 0x0f, 0xe0, 0x65,
|
||||
0xc7, 0x71, 0xfe, 0xdc, 0x60, 0x79, 0x4b, 0x13, 0x94, 0x12, 0x52, 0xca,
|
||||
0x13, 0x00, 0xd2, 0x3e, 0x4d, 0x83, 0x4a, 0x29, 0xdd, 0xf0, 0x3e, 0x72,
|
||||
0x22, 0xfd, 0xfa, 0x7c, 0x58, 0xb9, 0x1e, 0x14, 0x1f, 0xae, 0x07, 0xc5,
|
||||
0x87, 0xeb, 0x41, 0xf1, 0xe1, 0x7a, 0x50, 0x7c, 0x30, 0xa5, 0x94, 0x7f,
|
||||
0x22, 0xa2, 0xaa, 0x82, 0xc3, 0xcc, 0x13, 0xcc, 0xfc, 0x4b, 0xc7, 0x71,
|
||||
0xfe, 0x1e, 0x95, 0xb0, 0x7a, 0x11, 0x42, 0x6c, 0x26, 0xa2, 0x3d, 0xde,
|
||||
0xb8, 0xa8, 0x62, 0x98, 0xb9, 0x60, 0x02, 0xd8, 0xc2, 0xcc, 0x81, 0xb9,
|
||||
0x0d, 0x3f, 0x88, 0x68, 0x87, 0x94, 0x32, 0xa3, 0x94, 0x1a, 0xae, 0xb6,
|
||||
0x6f, 0xd4, 0x48, 0x29, 0x3f, 0x8f, 0xe2, 0xaa, 0xe3, 0x0a, 0xe6, 0x85,
|
||||
0xcb, 0x4a, 0x65, 0xb9, 0x6c, 0x00, 0xa8, 0xb5, 0xee, 0x63, 0x05, 0x33,
|
||||
0x3f, 0x51, 0x63, 0xdf, 0x48, 0xf1, 0x74, 0xd5, 0xba, 0xde, 0x9c, 0x37,
|
||||
0x00, 0xb8, 0xb5, 0x9e, 0x9c, 0x88, 0x6a, 0xa9, 0x35, 0x89, 0x9c, 0x3a,
|
||||
0x75, 0xe5, 0xeb, 0xb9, 0x53, 0xc0, 0xcc, 0xa7, 0xea, 0x38, 0x79, 0x64,
|
||||
0xd4, 0xa9, 0xcb, 0xad, 0xe7, 0x4e, 0xf9, 0x80, 0x88, 0x7e, 0x58, 0xc7,
|
||||
0xc9, 0x23, 0xc3, 0xd3, 0xf5, 0x41, 0x8d, 0xdd, 0xf3, 0x26, 0xf4, 0x77,
|
||||
0xca, 0x7b, 0x5e, 0x9a, 0x70, 0x11, 0xd7, 0xfa, 0xd7, 0x47, 0x29, 0x35,
|
||||
0x2c, 0x84, 0xe8, 0x0e, 0xfa, 0xfa, 0x30, 0x73, 0x12, 0xc0, 0x0d, 0x3e,
|
||||
0x4d, 0x79, 0x13, 0xc5, 0xba, 0x10, 0xbf, 0x67, 0xf0, 0xac, 0x6d, 0xdb,
|
||||
0xf7, 0x86, 0xa8, 0xb5, 0xa1, 0x78, 0xff, 0x61, 0xda, 0x05, 0x7a, 0x29,
|
||||
0xe5, 0x69, 0xf8, 0x07, 0xe5, 0x9c, 0x01, 0x60, 0x5c, 0xd3, 0x6f, 0x63,
|
||||
0x32, 0x99, 0xac, 0x25, 0x21, 0x74, 0xcd, 0xe3, 0x5d, 0xd7, 0x27, 0xfc,
|
||||
0xda, 0x98, 0x79, 0xdc, 0x00, 0xf0, 0xa6, 0xa6, 0x6f, 0xdc, 0xb2, 0xac,
|
||||
0xf6, 0xa8, 0x84, 0x2d, 0x25, 0xf1, 0x78, 0x7c, 0x33, 0xf4, 0x19, 0xc0,
|
||||
0x71, 0x83, 0x99, 0xb5, 0x2b, 0x6d, 0x44, 0x74, 0x57, 0x34, 0xb2, 0x96,
|
||||
0x16, 0x22, 0xd2, 0xad, 0x3a, 0xc2, 0x30, 0x8c, 0x31, 0xc3, 0x30, 0x0c,
|
||||
0x05, 0x60, 0xd6, 0xcf, 0x80, 0x99, 0xb5, 0x9d, 0x3f, 0xe4, 0xe8, 0xae,
|
||||
0xcb, 0x9d, 0x9e, 0x9e, 0x56, 0x86, 0xb7, 0x18, 0xfe, 0x17, 0x8d, 0xd1,
|
||||
0xd6, 0x4c, 0x26, 0x13, 0x56, 0x65, 0xe3, 0x35, 0x81, 0x10, 0x62, 0x75,
|
||||
0xc0, 0x9d, 0x32, 0x34, 0x3a, 0x3a, 0x7a, 0xa9, 0x34, 0x11, 0x3c, 0xaa,
|
||||
0x31, 0x5a, 0xe6, 0xba, 0x6e, 0x6f, 0x04, 0xda, 0x96, 0x0c, 0xc3, 0x30,
|
||||
0x7a, 0x01, 0x58, 0x9a, 0xe6, 0x63, 0x80, 0x97, 0x3a, 0x98, 0x9b, 0x9b,
|
||||
0x3b, 0x18, 0xe0, 0x67, 0x57, 0xc8, 0xba, 0x96, 0x14, 0x66, 0xd6, 0x5e,
|
||||
0x0f, 0x11, 0x1d, 0x04, 0xbc, 0xa0, 0x0c, 0x0d, 0x0d, 0x8d, 0x02, 0xf8,
|
||||
0x9b, 0xc6, 0xb6, 0x53, 0x4a, 0xf9, 0x7f, 0xf1, 0x6e, 0x91, 0x52, 0xde,
|
||||
0x07, 0xe0, 0x4e, 0x4d, 0xf3, 0xc9, 0x52, 0x25, 0xf7, 0xfc, 0x3c, 0xca,
|
||||
0x8b, 0x01, 0xfe, 0x7e, 0x10, 0x96, 0xb0, 0x25, 0x46, 0x7b, 0x1d, 0xcc,
|
||||
0xfc, 0x52, 0xe9, 0xdf, 0x57, 0x82, 0x92, 0xcf, 0xe7, 0x7f, 0x0b, 0x40,
|
||||
0x57, 0x96, 0x75, 0x97, 0x94, 0x32, 0xb2, 0xba, 0xd5, 0x46, 0x20, 0xa5,
|
||||
0xdc, 0x0e, 0x7d, 0x55, 0xd4, 0x85, 0x58, 0x2c, 0xf6, 0x42, 0xe9, 0x97,
|
||||
0x2b, 0x41, 0x19, 0x19, 0x19, 0xc9, 0x01, 0xf8, 0x75, 0x80, 0xdf, 0xbd,
|
||||
0x1d, 0x1d, 0x1d, 0x61, 0x56, 0x0f, 0x34, 0x0c, 0xaf, 0x96, 0xff, 0xa9,
|
||||
0x00, 0x93, 0x67, 0x06, 0x07, 0x07, 0xdf, 0x2b, 0xfd, 0x72, 0x55, 0x1a,
|
||||
0x72, 0x6e, 0x6e, 0xee, 0x17, 0x00, 0x2e, 0x6b, 0x3a, 0xae, 0x5f, 0xb9,
|
||||
0x72, 0xe5, 0xcf, 0xea, 0x97, 0xd8, 0x78, 0x2c, 0xcb, 0x7a, 0x0a, 0xfa,
|
||||
0x61, 0xfd, 0xa5, 0x58, 0x2c, 0xf6, 0xab, 0xf9, 0xc7, 0xae, 0x0a, 0xca,
|
||||
0xd0, 0xd0, 0xd0, 0x59, 0x00, 0x3f, 0xd7, 0x39, 0x67, 0xe6, 0xc7, 0x84,
|
||||
0x10, 0x0f, 0x85, 0xa0, 0xb3, 0x61, 0x08, 0x21, 0x1e, 0x04, 0xf0, 0x2d,
|
||||
0x5d, 0x3b, 0x11, 0xfd, 0xe4, 0xc4, 0x89, 0x13, 0xe7, 0xe7, 0x1f, 0x5b,
|
||||
0x94, 0xb0, 0x6e, 0x6a, 0x6a, 0xfa, 0x29, 0x00, 0x6d, 0x89, 0x38, 0x11,
|
||||
0xed, 0x97, 0x52, 0x6e, 0xaa, 0x47, 0x68, 0xa3, 0xf0, 0x92, 0xd7, 0xbf,
|
||||
0x09, 0x30, 0x39, 0x9d, 0x48, 0x24, 0x16, 0x3d, 0x56, 0x8b, 0x82, 0x92,
|
||||
0xcd, 0x66, 0x67, 0x88, 0x68, 0x77, 0x80, 0xa3, 0x04, 0x80, 0xd7, 0xa5,
|
||||
0x94, 0x1b, 0x6a, 0xd0, 0xd9, 0x30, 0xa4, 0x94, 0x1b, 0xbc, 0x6a, 0x6d,
|
||||
0xdd, 0xa6, 0x09, 0x26, 0xa2, 0xef, 0xf8, 0xed, 0x4d, 0xf4, 0x5d, 0xda,
|
||||
0xb0, 0x6d, 0xbb, 0x9f, 0x99, 0xf7, 0x05, 0x9c, 0xf3, 0xe3, 0x00, 0x8e,
|
||||
0x5c, 0xab, 0x81, 0xf1, 0x74, 0x1d, 0x41, 0x71, 0x17, 0xab, 0x8e, 0xbd,
|
||||
0xb6, 0x6d, 0x1f, 0xf1, 0x6b, 0xd0, 0xae, 0xf7, 0xb4, 0xb6, 0xb6, 0x7e,
|
||||
0x0f, 0x80, 0x6f, 0xe6, 0xcd, 0xe3, 0x16, 0x00, 0x4e, 0x3a, 0x9d, 0xfe,
|
||||
0x4c, 0x25, 0x42, 0x1b, 0x45, 0x2a, 0x95, 0x6a, 0x07, 0x30, 0x88, 0xa2,
|
||||
0x3e, 0x1d, 0xc3, 0xb9, 0x5c, 0xee, 0x71, 0x5d, 0xa3, 0x36, 0x28, 0xfd,
|
||||
0xfd, 0xfd, 0x79, 0x6f, 0x9e, 0x70, 0x5e, 0x67, 0x03, 0xe0, 0xa3, 0x85,
|
||||
0x42, 0xe1, 0x0d, 0x6f, 0x0c, 0xb0, 0xe4, 0xa4, 0x52, 0xa9, 0x5e, 0x66,
|
||||
0x56, 0x08, 0xde, 0x59, 0x76, 0xce, 0x34, 0xcd, 0x1d, 0x63, 0x63, 0x63,
|
||||
0xda, 0xdc, 0x74, 0xe0, 0xca, 0xe0, 0xe0, 0xe0, 0xe0, 0x19, 0x14, 0x4b,
|
||||
0xb2, 0x82, 0xca, 0xbd, 0x13, 0x00, 0x5e, 0x4d, 0xa5, 0x52, 0xfb, 0xa2,
|
||||
0xde, 0xdb, 0xa7, 0xa3, 0xa3, 0xa3, 0xa3, 0x59, 0x08, 0xb1, 0x9f, 0x99,
|
||||
0x0f, 0x40, 0xff, 0x0e, 0x01, 0x80, 0x8b, 0x44, 0xb4, 0x6d, 0x60, 0x60,
|
||||
0xe0, 0xdf, 0x41, 0xfe, 0xca, 0x2e, 0x97, 0x7a, 0x2b, 0x80, 0xdb, 0x51,
|
||||
0x26, 0xeb, 0xcf, 0xcc, 0x8f, 0x59, 0x96, 0xf5, 0x4f, 0x21, 0x44, 0x64,
|
||||
0x9b, 0x93, 0xfc, 0x90, 0x52, 0x6e, 0x6f, 0x69, 0x69, 0x39, 0x45, 0x44,
|
||||
0x8f, 0x96, 0x31, 0x75, 0x01, 0x6c, 0xb7, 0x6d, 0x5b, 0x97, 0x26, 0xb9,
|
||||
0x42, 0x45, 0x6b, 0xc8, 0x4a, 0xa9, 0xc3, 0x00, 0xbe, 0x82, 0xe0, 0x3b,
|
||||
0x06, 0x00, 0x36, 0x10, 0xd1, 0xef, 0x85, 0x10, 0xc7, 0xbb, 0xbb, 0xbb,
|
||||
0xb7, 0x56, 0xe2, 0xbb, 0x56, 0xa4, 0x94, 0xf7, 0x49, 0x29, 0x07, 0x00,
|
||||
0xbc, 0x0a, 0xa0, 0xdc, 0x7a, 0xf1, 0x45, 0x22, 0xfa, 0xb2, 0x77, 0x1d,
|
||||
0x65, 0xa9, 0x78, 0xb7, 0xa9, 0x52, 0xea, 0xb0, 0x37, 0x5b, 0x7e, 0x0d,
|
||||
0xc0, 0x9a, 0x20, 0x5b, 0x22, 0xca, 0x10, 0x51, 0x46, 0x4a, 0x39, 0x4c,
|
||||
0x44, 0xbf, 0x23, 0xa2, 0x03, 0xf3, 0x87, 0xd1, 0xb5, 0x22, 0x84, 0x58,
|
||||
0x6d, 0x18, 0x46, 0xaf, 0x37, 0xfd, 0xd7, 0xcd, 0x76, 0x17, 0x72, 0x8e,
|
||||
0x88, 0xb6, 0x55, 0x72, 0x87, 0x94, 0xa8, 0xba, 0xe6, 0x2d, 0x9d, 0x4e,
|
||||
0x7f, 0xb2, 0x50, 0x28, 0x1c, 0xa8, 0x42, 0x14, 0x00, 0xe4, 0x99, 0xf9,
|
||||
0x18, 0x11, 0x1d, 0x65, 0xe6, 0x63, 0xae, 0xeb, 0xfe, 0x63, 0x64, 0x64,
|
||||
0x64, 0xb6, 0x5c, 0xcd, 0x5b, 0x32, 0x99, 0x8c, 0xc7, 0xe3, 0xf1, 0xcd,
|
||||
0x5e, 0xa6, 0xec, 0x6e, 0xef, 0xa7, 0x2e, 0x41, 0xe4, 0xc7, 0xb0, 0x69,
|
||||
0x9a, 0x3b, 0xca, 0xbd, 0x43, 0x16, 0x52, 0x53, 0x21, 0x60, 0x4f, 0x4f,
|
||||
0x8f, 0x95, 0xcb, 0xe5, 0x9e, 0x04, 0x10, 0x34, 0xc8, 0x0b, 0xe2, 0xbf,
|
||||
0x00, 0xce, 0xa0, 0xb8, 0xc5, 0xd6, 0x6f, 0xab, 0xff, 0x14, 0x8a, 0x7f,
|
||||
0xfc, 0xe1, 0x66, 0xd4, 0xb6, 0x77, 0x9a, 0x01, 0xec, 0xcd, 0xe5, 0x72,
|
||||
0x8f, 0x07, 0x7d, 0x65, 0x74, 0xd4, 0x55, 0x1d, 0x29, 0x84, 0xf8, 0x12,
|
||||
0x11, 0xed, 0x43, 0xf9, 0x0d, 0xd9, 0x8d, 0xe4, 0x34, 0x80, 0xdd, 0x95,
|
||||
0xbe, 0x3f, 0xfc, 0xa8, 0xab, 0xb8, 0x78, 0x72, 0x72, 0xf2, 0xad, 0x8d,
|
||||
0x1b, 0x37, 0x3e, 0x5b, 0x28, 0x14, 0x66, 0x01, 0x74, 0xa1, 0xb6, 0x6a,
|
||||
0xea, 0x50, 0x60, 0xe6, 0x4b, 0x44, 0xf4, 0xa3, 0x44, 0x22, 0xb1, 0xf3,
|
||||
0xf8, 0xf1, 0xe3, 0xa7, 0xeb, 0xf1, 0x15, 0x5a, 0x1d, 0xad, 0x94, 0x72,
|
||||
0x2d, 0x11, 0xed, 0x61, 0xe6, 0x6f, 0x23, 0x78, 0xac, 0x10, 0x36, 0x17,
|
||||
0x00, 0xec, 0x8b, 0xc5, 0x62, 0x4f, 0x2f, 0x9c, 0xed, 0xd6, 0x4a, 0xe8,
|
||||
0xc5, 0xc5, 0xe9, 0x74, 0xfa, 0x06, 0x66, 0x7e, 0x98, 0x99, 0x77, 0x02,
|
||||
0xf8, 0x6c, 0xd8, 0xfe, 0xe7, 0x71, 0x92, 0x99, 0x5f, 0xb2, 0x2c, 0xeb,
|
||||
0xf9, 0x6c, 0x36, 0x1b, 0xea, 0x06, 0xcb, 0xa8, 0xf7, 0xfb, 0x6c, 0x62,
|
||||
0xe6, 0x07, 0x0d, 0xc3, 0xb8, 0x87, 0x99, 0x3b, 0x51, 0xdf, 0xe3, 0x35,
|
||||
0x0b, 0x60, 0x08, 0xc0, 0x51, 0x22, 0x3a, 0x18, 0xe5, 0x9f, 0x0b, 0x69,
|
||||
0x58, 0x19, 0x7a, 0x26, 0x93, 0x69, 0x99, 0x9d, 0x9d, 0x15, 0x00, 0x36,
|
||||
0x01, 0xb8, 0x8d, 0x99, 0x6f, 0x45, 0x71, 0x16, 0x7b, 0x13, 0x11, 0xb5,
|
||||
0x32, 0x73, 0x8c, 0x88, 0xe6, 0x98, 0xf9, 0x7d, 0x14, 0xbf, 0x3c, 0x6f,
|
||||
0x33, 0xf3, 0x38, 0x80, 0x71, 0xc3, 0x30, 0xc6, 0xa6, 0xa7, 0xa7, 0xd5,
|
||||
0xe8, 0xe8, 0xe8, 0xa5, 0x46, 0x68, 0xfd, 0x1f, 0x32, 0x61, 0xc5, 0x44,
|
||||
0x9c, 0xe8, 0x1b, 0xc4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44,
|
||||
0xae, 0x42, 0x60, 0x82,
|
||||
}
|
||||
|
||||
8828
platform/icon_win.go
34
readme.md
@@ -1,16 +1,36 @@
|
||||
# yarr
|
||||
|
||||
yet another rss reader.
|
||||
**yarr** (yet another rss reader) is a web-based feed aggregator which can be used both
|
||||
as a desktop application and a personal self-hosted server.
|
||||
|
||||

|
||||
The app is a single binary with an embedded database (SQLite).
|
||||
|
||||
*yarr* is a server written in Go with the frontend in Vue.js. The storage is backed by SQLite.
|
||||

|
||||
|
||||
The goal of the project is to provide a desktop application accessible via web browser.
|
||||
Longer-term plans include a self-hosted solution for individuals.
|
||||
Support for 3rd-party applications (via Fever API) is being considered.
|
||||
## usage
|
||||
|
||||
[download](https://github.com/nkanaev/yarr/releases/latest)
|
||||
The latest prebuilt binaries for Linux/MacOS/Windows AMD64 are available
|
||||
[here](https://github.com/nkanaev/yarr/releases/latest).
|
||||
|
||||
### macos
|
||||
|
||||
Download `yarr-*-macos64.zip`, unzip it, place `yarr.app` in `/Applications` folder, [open the app][macos-open], click the anchor menu bar icon, select "Open".
|
||||
|
||||
[macos-open]: https://support.apple.com/en-gb/guide/mac-help/mh40616/mac
|
||||
|
||||
### windows
|
||||
|
||||
Download `yarr-*-windows64.zip`, unzip it, open `yarr.exe`, click the anchor system tray icon, select "Open".
|
||||
|
||||
### linux
|
||||
|
||||
Download `yarr-*-linux64.zip`, unzip it, place `yarr` in `$HOME/.local/bin`
|
||||
and run [the script](etc/install-linux.sh).
|
||||
|
||||
For self-hosting, see `yarr -h` for auth, tls & server configuration flags.
|
||||
For building from source code, see [build.md](build.md)
|
||||
|
||||
For Fever API support, see [fever.md](fever.md).
|
||||
|
||||
## credits
|
||||
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
htemplate "html/template"
|
||||
)
|
||||
|
||||
var code_template = `// +build release
|
||||
|
||||
// autogenerated. do not edit!
|
||||
|
||||
package server
|
||||
|
||||
var assets_bundle = map[string]asset{
|
||||
{{- range .}}
|
||||
"{{.Name}}": {etag: "{{.Etag}}", body: "{{.Body}}"},
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
func init() {
|
||||
assets = assets_bundle
|
||||
}
|
||||
`
|
||||
|
||||
type asset struct {
|
||||
Name, Etag, Body string
|
||||
}
|
||||
|
||||
func shasum(b []byte) string {
|
||||
h := sha256.New()
|
||||
h.Write(b)
|
||||
return fmt.Sprintf("%x", h.Sum(nil))[:16]
|
||||
}
|
||||
|
||||
func encode(b []byte) string {
|
||||
var buf bytes.Buffer
|
||||
zw := gzip.NewWriter(&buf)
|
||||
zw.Write(b)
|
||||
zw.Close()
|
||||
return base64.StdEncoding.EncodeToString(buf.Bytes())
|
||||
}
|
||||
|
||||
func main() {
|
||||
assets := make([]asset, 0)
|
||||
filepatterns := []string{
|
||||
"assets/graphicarts/*.svg",
|
||||
"assets/graphicarts/*.png",
|
||||
"assets/javascripts/*.js",
|
||||
"assets/stylesheets/*.css",
|
||||
"assets/stylesheets/*.map",
|
||||
}
|
||||
fmt.Printf("%8s %8s %s\n", "original", "encoded", "filename")
|
||||
for _, pattern := range filepatterns {
|
||||
filenames, _ := filepath.Glob(pattern)
|
||||
for _, filename := range filenames {
|
||||
content, _ := ioutil.ReadFile(filename)
|
||||
assets = append(assets, asset{
|
||||
Name: strings.TrimPrefix(strings.ReplaceAll(filename, "\\", "/"), "assets/"),
|
||||
Etag: shasum(content),
|
||||
Body: encode(content),
|
||||
})
|
||||
fmt.Printf(
|
||||
"%8d %8d %s\n",
|
||||
len(content),
|
||||
len(assets[len(assets)-1].Body),
|
||||
filename,
|
||||
)
|
||||
}
|
||||
}
|
||||
var indexbuf bytes.Buffer
|
||||
htemplate.Must(htemplate.New("index.html").Delims("{%", "%}").Funcs(htemplate.FuncMap{
|
||||
"inline": func(svg string) htemplate.HTML {
|
||||
content, _ := ioutil.ReadFile("assets/graphicarts/" + svg)
|
||||
return htemplate.HTML(content)
|
||||
},
|
||||
}).ParseFiles("assets/index.html")).Execute(&indexbuf, nil)
|
||||
indexcontent := indexbuf.Bytes()
|
||||
assets = append(assets, asset{
|
||||
Name: "index.html",
|
||||
Etag: shasum(indexcontent),
|
||||
Body: encode(indexcontent),
|
||||
})
|
||||
|
||||
var buf bytes.Buffer
|
||||
template := template.Must(template.New("code").Parse(code_template))
|
||||
template.Execute(&buf, assets)
|
||||
ioutil.WriteFile("server/assets.go", buf.Bytes(), 0644)
|
||||
}
|
||||
@@ -1,281 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/mmcdole/gofeed"
|
||||
"github.com/nkanaev/yarr/storage"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type FeedSource struct {
|
||||
Title string `json:"title"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
const feedLinks = `
|
||||
link[type='application/rss+xml'],
|
||||
link[type='application/atom+xml'],
|
||||
a[href$="/feed"],
|
||||
a[href$="/feed/"],
|
||||
a[href$="feed.xml"],
|
||||
a[href$="atom.xml"],
|
||||
a[href$="rss.xml"],
|
||||
a:contains("rss"),
|
||||
a:contains("RSS"),
|
||||
a:contains("feed"),
|
||||
a:contains("FEED")
|
||||
`
|
||||
|
||||
type Client struct {
|
||||
httpClient *http.Client
|
||||
userAgent string
|
||||
}
|
||||
|
||||
func (c *Client) get(url string) (*http.Response, error) {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", c.userAgent)
|
||||
return c.httpClient.Do(req)
|
||||
}
|
||||
|
||||
var defaultClient *Client
|
||||
|
||||
func searchFeedLinks(html []byte, siteurl string) ([]FeedSource, error) {
|
||||
sources := make([]FeedSource, 0, 0)
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(bytes.NewReader(html))
|
||||
if err != nil {
|
||||
return sources, err
|
||||
}
|
||||
base, err := url.Parse(siteurl)
|
||||
if err != nil {
|
||||
return sources, err
|
||||
}
|
||||
|
||||
// feed {url: title} map
|
||||
feeds := make(map[string]string)
|
||||
|
||||
doc.Find(feedLinks).Each(func(i int, s *goquery.Selection) {
|
||||
// Unlikely to happen, but don't get more than N links
|
||||
if len(feeds) > 10 {
|
||||
return
|
||||
}
|
||||
if href, ok := s.Attr("href"); ok {
|
||||
feedUrl, err := url.Parse(href)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
title := s.AttrOr("title", "")
|
||||
url := base.ResolveReference(feedUrl).String()
|
||||
|
||||
if _, alreadyExists := feeds[url]; alreadyExists {
|
||||
if feeds[url] == "" {
|
||||
feeds[url] = title
|
||||
}
|
||||
} else {
|
||||
feeds[url] = title
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
for url, title := range feeds {
|
||||
sources = append(sources, FeedSource{Title: title, Url: url})
|
||||
}
|
||||
return sources, nil
|
||||
}
|
||||
|
||||
func discoverFeed(candidateUrl string) (*gofeed.Feed, *[]FeedSource, error) {
|
||||
// Query URL
|
||||
res, err := defaultClient.get(candidateUrl)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
errmsg := fmt.Sprintf("Failed to fetch feed %s (status: %d)", candidateUrl, res.StatusCode)
|
||||
return nil, nil, errors.New(errmsg)
|
||||
}
|
||||
content, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Try to feed into parser
|
||||
feedparser := gofeed.NewParser()
|
||||
feed, err := feedparser.Parse(bytes.NewReader(content))
|
||||
if err == nil {
|
||||
// WILD: feeds may not always have link to themselves
|
||||
if len(feed.FeedLink) == 0 {
|
||||
feed.FeedLink = candidateUrl
|
||||
}
|
||||
|
||||
// WILD: resolve relative links (path, without host)
|
||||
base, _ := url.Parse(candidateUrl)
|
||||
if link, err := url.Parse(feed.Link); err == nil && link.Host == "" {
|
||||
feed.Link = base.ResolveReference(link).String()
|
||||
}
|
||||
if link, err := url.Parse(feed.FeedLink); err == nil && link.Host == "" {
|
||||
feed.FeedLink = base.ResolveReference(link).String()
|
||||
}
|
||||
|
||||
return feed, nil, nil
|
||||
}
|
||||
|
||||
// Possibly an html link. Search for feed links
|
||||
sources, err := searchFeedLinks(content, candidateUrl)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
} else if len(sources) == 0 {
|
||||
return nil, nil, errors.New("No feeds found at the given url")
|
||||
} else if len(sources) == 1 {
|
||||
if sources[0].Url == candidateUrl {
|
||||
return nil, nil, errors.New("Recursion!")
|
||||
}
|
||||
return discoverFeed(sources[0].Url)
|
||||
}
|
||||
return nil, &sources, nil
|
||||
}
|
||||
|
||||
func findFavicon(websiteUrl, feedUrl string) (*[]byte, error) {
|
||||
candidateUrls := make([]string, 0)
|
||||
|
||||
favicon := func(link string) string {
|
||||
u, err := url.Parse(link)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s://%s/favicon.ico", u.Scheme, u.Host)
|
||||
}
|
||||
|
||||
if len(websiteUrl) != 0 {
|
||||
base, err := url.Parse(websiteUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := defaultClient.get(websiteUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
doc, err := goquery.NewDocumentFromReader(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
doc.Find(`link[rel=icon]`).EachWithBreak(func(i int, s *goquery.Selection) bool {
|
||||
if href, ok := s.Attr("href"); ok {
|
||||
if hrefUrl, err := url.Parse(href); err == nil {
|
||||
faviconUrl := base.ResolveReference(hrefUrl).String()
|
||||
candidateUrls = append(candidateUrls, faviconUrl)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if c := favicon(websiteUrl); len(c) != 0 {
|
||||
candidateUrls = append(candidateUrls, c)
|
||||
}
|
||||
}
|
||||
if c := favicon(feedUrl); len(c) != 0 {
|
||||
candidateUrls = append(candidateUrls, c)
|
||||
}
|
||||
|
||||
imageTypes := [4]string{
|
||||
"image/x-icon",
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/gif",
|
||||
}
|
||||
for _, url := range candidateUrls {
|
||||
res, err := defaultClient.get(url)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode == 200 {
|
||||
if content, err := ioutil.ReadAll(res.Body); err == nil {
|
||||
ctype := http.DetectContentType(content)
|
||||
for _, itype := range imageTypes {
|
||||
if ctype == itype {
|
||||
return &content, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func convertItems(items []*gofeed.Item, feed storage.Feed) []storage.Item {
|
||||
result := make([]storage.Item, len(items))
|
||||
for i, item := range items {
|
||||
imageURL := ""
|
||||
if item.Image != nil {
|
||||
imageURL = item.Image.URL
|
||||
}
|
||||
author := ""
|
||||
if item.Author != nil {
|
||||
author = item.Author.Name
|
||||
}
|
||||
result[i] = storage.Item{
|
||||
GUID: item.GUID,
|
||||
FeedId: feed.Id,
|
||||
Title: item.Title,
|
||||
Link: item.Link,
|
||||
Description: item.Description,
|
||||
Content: item.Content,
|
||||
Author: author,
|
||||
Date: item.PublishedParsed,
|
||||
DateUpdated: item.UpdatedParsed,
|
||||
Status: storage.UNREAD,
|
||||
Image: imageURL,
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func listItems(f storage.Feed) ([]storage.Item, error) {
|
||||
res, err := defaultClient.get(f.FeedLink)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode == 404 {
|
||||
errmsg := fmt.Sprintf("Failed to list feed items for %s (status: 404)", f.FeedLink)
|
||||
return nil, errors.New(errmsg)
|
||||
}
|
||||
feedparser := gofeed.NewParser()
|
||||
feed, err := feedparser.Parse(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertItems(feed.Items, f), nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
transport := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 10 * time.Second,
|
||||
}).DialContext,
|
||||
DisableKeepAlives: true,
|
||||
TLSHandshakeTimeout: time.Second * 10,
|
||||
}
|
||||
httpClient := &http.Client{
|
||||
Timeout: time.Second * 30,
|
||||
Transport: transport,
|
||||
}
|
||||
defaultClient = &Client{
|
||||
httpClient: httpClient,
|
||||
userAgent: "Yarr/1.0",
|
||||
}
|
||||
}
|
||||
@@ -1,482 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/base64"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"github.com/nkanaev/yarr/storage"
|
||||
"html"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var routes []Route = []Route{
|
||||
p("/", IndexHandler),
|
||||
p("/static/*path", StaticHandler),
|
||||
p("/api/status", StatusHandler),
|
||||
p("/api/folders", FolderListHandler),
|
||||
p("/api/folders/:id", FolderHandler),
|
||||
p("/api/feeds", FeedListHandler),
|
||||
p("/api/feeds/find", FeedHandler),
|
||||
p("/api/feeds/refresh", FeedRefreshHandler),
|
||||
p("/api/feeds/:id/icon", FeedIconHandler),
|
||||
p("/api/feeds/:id", FeedHandler),
|
||||
p("/api/items", ItemListHandler),
|
||||
p("/api/items/:id", ItemHandler),
|
||||
p("/api/settings", SettingsHandler),
|
||||
p("/opml/import", OPMLImportHandler),
|
||||
p("/opml/export", OPMLExportHandler),
|
||||
p("/page", PageCrawlHandler),
|
||||
}
|
||||
|
||||
type asset struct {
|
||||
etag string
|
||||
body string // base64(gzip(content))
|
||||
gzipped *[]byte
|
||||
decoded *string
|
||||
}
|
||||
|
||||
func (a *asset) gzip() *[]byte {
|
||||
if a.gzipped == nil {
|
||||
gzipped, _ := base64.StdEncoding.DecodeString(a.body)
|
||||
a.gzipped = &gzipped
|
||||
}
|
||||
return a.gzipped
|
||||
}
|
||||
|
||||
func (a *asset) text() *string {
|
||||
if a.decoded == nil {
|
||||
gzipped, _ := base64.StdEncoding.DecodeString(a.body)
|
||||
reader, _ := gzip.NewReader(bytes.NewBuffer(gzipped))
|
||||
decoded, _ := ioutil.ReadAll(reader)
|
||||
reader.Close()
|
||||
|
||||
decoded_string := string(decoded)
|
||||
a.decoded = &decoded_string
|
||||
}
|
||||
return a.decoded
|
||||
}
|
||||
|
||||
var assets map[string]asset
|
||||
|
||||
type FolderCreateForm struct {
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
type FolderUpdateForm struct {
|
||||
Title *string `json:"title,omitempty"`
|
||||
IsExpanded *bool `json:"is_expanded,omitempty"`
|
||||
}
|
||||
|
||||
type FeedCreateForm struct {
|
||||
Url string `json:"url"`
|
||||
FolderID *int64 `json:"folder_id,omitempty"`
|
||||
}
|
||||
|
||||
type ItemUpdateForm struct {
|
||||
Status *storage.ItemStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
func IndexHandler(rw http.ResponseWriter, req *http.Request) {
|
||||
if assets != nil {
|
||||
asset := assets["index.html"]
|
||||
|
||||
rw.Header().Set("Content-Type", "text/html")
|
||||
rw.Header().Set("Content-Encoding", "gzip")
|
||||
rw.Write(*asset.gzip())
|
||||
} else {
|
||||
t := template.Must(template.New("index.html").Delims("{%", "%}").Funcs(template.FuncMap{
|
||||
"inline": func(svg string) template.HTML {
|
||||
content, _ := ioutil.ReadFile("assets/graphicarts/" + svg)
|
||||
return template.HTML(content)
|
||||
},
|
||||
}).ParseFiles("assets/index.html"))
|
||||
rw.Header().Set("Content-Type", "text/html")
|
||||
t.Execute(rw, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func StaticHandler(rw http.ResponseWriter, req *http.Request) {
|
||||
path := Vars(req)["path"]
|
||||
ctype := mime.TypeByExtension(filepath.Ext(path))
|
||||
|
||||
if assets != nil {
|
||||
if asset, ok := assets[path]; ok {
|
||||
if req.Header.Get("if-none-match") == asset.etag {
|
||||
rw.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
rw.Header().Set("Content-Type", ctype)
|
||||
rw.Header().Set("Content-Encoding", "gzip")
|
||||
rw.Header().Set("Etag", asset.etag)
|
||||
rw.Write(*asset.gzip())
|
||||
}
|
||||
}
|
||||
|
||||
f, err := os.Open("assets/" + path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
rw.Header().Set("Content-Type", ctype)
|
||||
io.Copy(rw, f)
|
||||
}
|
||||
|
||||
func StatusHandler(rw http.ResponseWriter, req *http.Request) {
|
||||
writeJSON(rw, map[string]interface{}{
|
||||
"running": *handler(req).queueSize,
|
||||
"stats": db(req).FeedStats(),
|
||||
})
|
||||
}
|
||||
|
||||
func FolderListHandler(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method == "GET" {
|
||||
list := db(req).ListFolders()
|
||||
writeJSON(rw, list)
|
||||
} else if req.Method == "POST" {
|
||||
var body FolderCreateForm
|
||||
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
|
||||
handler(req).log.Print(err)
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if len(body.Title) == 0 {
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
writeJSON(rw, map[string]string{"error": "Folder title missing."})
|
||||
return
|
||||
}
|
||||
folder := db(req).CreateFolder(body.Title)
|
||||
rw.WriteHeader(http.StatusCreated)
|
||||
writeJSON(rw, folder)
|
||||
} else {
|
||||
rw.WriteHeader(http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func FolderHandler(rw http.ResponseWriter, req *http.Request) {
|
||||
id, err := strconv.ParseInt(Vars(req)["id"], 10, 64)
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.Method == "PUT" {
|
||||
var body FolderUpdateForm
|
||||
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
|
||||
handler(req).log.Print(err)
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if body.Title != nil {
|
||||
db(req).RenameFolder(id, *body.Title)
|
||||
}
|
||||
if body.IsExpanded != nil {
|
||||
db(req).ToggleFolderExpanded(id, *body.IsExpanded)
|
||||
}
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
} else if req.Method == "DELETE" {
|
||||
db(req).DeleteFolder(id)
|
||||
rw.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
func FeedRefreshHandler(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method == "POST" {
|
||||
handler(req).fetchAllFeeds()
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
} else {
|
||||
rw.WriteHeader(http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func FeedIconHandler(rw http.ResponseWriter, req *http.Request) {
|
||||
id, err := strconv.ParseInt(Vars(req)["id"], 10, 64)
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
feed := db(req).GetFeed(id)
|
||||
if feed != nil && feed.Icon != nil {
|
||||
rw.Header().Set("Content-Type", http.DetectContentType(*feed.Icon))
|
||||
rw.Header().Set("Content-Length", strconv.Itoa(len(*feed.Icon)))
|
||||
rw.Write(*feed.Icon)
|
||||
} else {
|
||||
rw.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func FeedListHandler(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method == "GET" {
|
||||
list := db(req).ListFeeds()
|
||||
writeJSON(rw, list)
|
||||
} else if req.Method == "POST" {
|
||||
var form FeedCreateForm
|
||||
if err := json.NewDecoder(req.Body).Decode(&form); err != nil {
|
||||
handler(req).log.Print(err)
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
feed, sources, err := discoverFeed(form.Url)
|
||||
if err != nil {
|
||||
handler(req).log.Print(err)
|
||||
writeJSON(rw, map[string]string{"status": "notfound"})
|
||||
return
|
||||
}
|
||||
|
||||
if feed != nil {
|
||||
storedFeed := db(req).CreateFeed(
|
||||
feed.Title,
|
||||
feed.Description,
|
||||
feed.Link,
|
||||
feed.FeedLink,
|
||||
form.FolderID,
|
||||
)
|
||||
db(req).CreateItems(convertItems(feed.Items, *storedFeed))
|
||||
writeJSON(rw, map[string]string{"status": "success"})
|
||||
} else if sources != nil {
|
||||
writeJSON(rw, map[string]interface{}{"status": "multiple", "choice": sources})
|
||||
} else {
|
||||
writeJSON(rw, map[string]string{"status": "notfound"})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FeedHandler(rw http.ResponseWriter, req *http.Request) {
|
||||
id, err := strconv.ParseInt(Vars(req)["id"], 10, 64)
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.Method == "PUT" {
|
||||
feed := db(req).GetFeed(id)
|
||||
if feed == nil {
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
body := make(map[string]interface{})
|
||||
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
|
||||
handler(req).log.Print(err)
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if title, ok := body["title"]; ok {
|
||||
if reflect.TypeOf(title).Kind() == reflect.String {
|
||||
db(req).RenameFeed(id, title.(string))
|
||||
}
|
||||
}
|
||||
if f_id, ok := body["folder_id"]; ok {
|
||||
if f_id == nil {
|
||||
db(req).UpdateFeedFolder(id, nil)
|
||||
} else if reflect.TypeOf(f_id).Kind() == reflect.Float64 {
|
||||
folderId := int64(f_id.(float64))
|
||||
db(req).UpdateFeedFolder(id, &folderId)
|
||||
}
|
||||
}
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
} else if req.Method == "DELETE" {
|
||||
db(req).DeleteFeed(id)
|
||||
rw.WriteHeader(http.StatusNoContent)
|
||||
} else {
|
||||
rw.WriteHeader(http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func ItemHandler(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method == "PUT" {
|
||||
id, err := strconv.ParseInt(Vars(req)["id"], 10, 64)
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var body ItemUpdateForm
|
||||
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
|
||||
handler(req).log.Print(err)
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if body.Status != nil {
|
||||
db(req).UpdateItemStatus(id, *body.Status)
|
||||
}
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
} else {
|
||||
rw.WriteHeader(http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func ItemListHandler(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method == "GET" {
|
||||
perPage := 20
|
||||
curPage := 1
|
||||
query := req.URL.Query()
|
||||
if page, err := strconv.ParseInt(query.Get("page"), 10, 64); err == nil {
|
||||
curPage = int(page)
|
||||
}
|
||||
filter := storage.ItemFilter{}
|
||||
if folderID, err := strconv.ParseInt(query.Get("folder_id"), 10, 64); err == nil {
|
||||
filter.FolderID = &folderID
|
||||
}
|
||||
if feedID, err := strconv.ParseInt(query.Get("feed_id"), 10, 64); err == nil {
|
||||
filter.FeedID = &feedID
|
||||
}
|
||||
if status := query.Get("status"); len(status) != 0 {
|
||||
statusValue := storage.StatusValues[status]
|
||||
filter.Status = &statusValue
|
||||
}
|
||||
if search := query.Get("search"); len(search) != 0 {
|
||||
filter.Search = &search
|
||||
}
|
||||
newestFirst := query.Get("oldest_first") != "true"
|
||||
items := db(req).ListItems(filter, (curPage-1)*perPage, perPage, newestFirst)
|
||||
count := db(req).CountItems(filter)
|
||||
writeJSON(rw, map[string]interface{}{
|
||||
"page": map[string]int{
|
||||
"cur": curPage,
|
||||
"num": int(math.Ceil(float64(count) / float64(perPage))),
|
||||
},
|
||||
"list": items,
|
||||
})
|
||||
} else if req.Method == "PUT" {
|
||||
query := req.URL.Query()
|
||||
filter := storage.ItemFilter{}
|
||||
if folderID, err := strconv.ParseInt(query.Get("folder_id"), 10, 64); err == nil {
|
||||
filter.FolderID = &folderID
|
||||
}
|
||||
if feedID, err := strconv.ParseInt(query.Get("feed_id"), 10, 64); err == nil {
|
||||
filter.FeedID = &feedID
|
||||
}
|
||||
db(req).MarkItemsRead(filter)
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
} else {
|
||||
rw.WriteHeader(http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func SettingsHandler(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method == "GET" {
|
||||
writeJSON(rw, db(req).GetSettings())
|
||||
} else if req.Method == "PUT" {
|
||||
settings := make(map[string]interface{})
|
||||
if err := json.NewDecoder(req.Body).Decode(&settings); err != nil {
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if db(req).UpdateSettings(settings) {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
} else {
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func OPMLImportHandler(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method == "POST" {
|
||||
file, _, err := req.FormFile("opml")
|
||||
if err != nil {
|
||||
handler(req).log.Print(err)
|
||||
return
|
||||
}
|
||||
doc, err := parseOPML(file)
|
||||
if err != nil {
|
||||
handler(req).log.Print(err)
|
||||
return
|
||||
}
|
||||
for _, outline := range doc.Outlines {
|
||||
if outline.Type == "rss" {
|
||||
db(req).CreateFeed(outline.Title, outline.Description, outline.SiteURL, outline.FeedURL, nil)
|
||||
} else {
|
||||
folder := db(req).CreateFolder(outline.Title)
|
||||
for _, o := range outline.AllFeeds() {
|
||||
db(req).CreateFeed(o.Title, o.Description, o.SiteURL, o.FeedURL, &folder.Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
handler(req).fetchAllFeeds()
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
} else {
|
||||
rw.WriteHeader(http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func OPMLExportHandler(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method == "GET" {
|
||||
rw.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
||||
rw.Header().Set("Content-Disposition", `attachment; filename="subscriptions.opml"`)
|
||||
|
||||
builder := strings.Builder{}
|
||||
|
||||
line := func(s string, args ...string) {
|
||||
if len(args) > 0 {
|
||||
escapedargs := make([]interface{}, len(args))
|
||||
for idx, arg := range args {
|
||||
escapedargs[idx] = html.EscapeString(arg)
|
||||
}
|
||||
s = fmt.Sprintf(s, escapedargs...)
|
||||
}
|
||||
builder.WriteString(s)
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
feedline := func(feed storage.Feed, indent int) {
|
||||
line(
|
||||
strings.Repeat(" ", indent)+
|
||||
`<outline type="rss" text="%s" description="%s" xmlUrl="%s" htmlUrl="%s"/>`,
|
||||
feed.Title, feed.Description,
|
||||
feed.FeedLink, feed.Link,
|
||||
)
|
||||
}
|
||||
line(`<?xml version="1.0" encoding="UTF-8"?>`)
|
||||
line(`<opml version="1.1">`)
|
||||
line(`<head>`)
|
||||
line(` <title>subscriptions.opml</title>`)
|
||||
line(`</head>`)
|
||||
line(`<body>`)
|
||||
feedsByFolderID := make(map[int64][]storage.Feed)
|
||||
for _, feed := range db(req).ListFeeds() {
|
||||
var folderId = int64(0)
|
||||
if feed.FolderId != nil {
|
||||
folderId = *feed.FolderId
|
||||
}
|
||||
if feedsByFolderID[folderId] == nil {
|
||||
feedsByFolderID[folderId] = make([]storage.Feed, 0)
|
||||
}
|
||||
feedsByFolderID[folderId] = append(feedsByFolderID[folderId], feed)
|
||||
}
|
||||
for _, folder := range db(req).ListFolders() {
|
||||
line(` <outline text="%s">`, folder.Title)
|
||||
for _, feed := range feedsByFolderID[folder.Id] {
|
||||
feedline(feed, 4)
|
||||
}
|
||||
line(` </outline>`)
|
||||
}
|
||||
for _, feed := range feedsByFolderID[0] {
|
||||
feedline(feed, 2)
|
||||
}
|
||||
line(`</body>`)
|
||||
line(`</opml>`)
|
||||
rw.Write([]byte(builder.String()))
|
||||
}
|
||||
}
|
||||
|
||||
func PageCrawlHandler(rw http.ResponseWriter, req *http.Request) {
|
||||
query := req.URL.Query()
|
||||
if url := query.Get("url"); len(url) > 0 {
|
||||
res, err := http.Get(url)
|
||||
if err == nil {
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err == nil {
|
||||
rw.Write(body)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"io"
|
||||
)
|
||||
|
||||
type opml struct {
|
||||
XMLName xml.Name `xml:"opml"`
|
||||
Version string `xml:"version,attr"`
|
||||
Outlines []outline `xml:"body>outline"`
|
||||
}
|
||||
|
||||
type outline struct {
|
||||
Type string `xml:"type,attr,omitempty"`
|
||||
Title string `xml:"text,attr"`
|
||||
FeedURL string `xml:"xmlUrl,attr,omitempty"`
|
||||
SiteURL string `xml:"htmlUrl,attr,omitempty"`
|
||||
Description string `xml:"description,attr,omitempty"`
|
||||
Outlines []outline `xml:"outline,omitempty"`
|
||||
}
|
||||
|
||||
func (o outline) AllFeeds() []outline {
|
||||
result := make([]outline, 0)
|
||||
for _, sub := range o.Outlines {
|
||||
if sub.Type == "rss" {
|
||||
result = append(result, sub)
|
||||
} else {
|
||||
result = append(result, sub.AllFeeds()...)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func parseOPML(r io.Reader) (*opml, error) {
|
||||
feeds := new(opml)
|
||||
decoder := xml.NewDecoder(r)
|
||||
decoder.Entity = xml.HTMLEntity
|
||||
decoder.Strict = false
|
||||
err := decoder.Decode(&feeds)
|
||||
return feeds, err
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"log"
|
||||
)
|
||||
|
||||
func writeJSON(rw http.ResponseWriter, data interface{}) {
|
||||
rw.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
reply, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
rw.Write(reply)
|
||||
rw.Write([]byte("\n"))
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type Route struct {
|
||||
url string
|
||||
urlRegex *regexp.Regexp
|
||||
handler func(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
func p(path string, handler func(http.ResponseWriter, *http.Request)) Route {
|
||||
var urlRegexp string
|
||||
urlRegexp = regexp.MustCompile(`[\*\:]\w+`).ReplaceAllStringFunc(path, func(m string) string {
|
||||
if m[0:1] == `*` {
|
||||
return "(?P<" + m[1:] + ">.+)"
|
||||
}
|
||||
return "(?P<" + m[1:] + ">[^/]+)"
|
||||
})
|
||||
urlRegexp = "^" + urlRegexp + "$"
|
||||
return Route{
|
||||
url: path,
|
||||
urlRegex: regexp.MustCompile(urlRegexp),
|
||||
handler: handler,
|
||||
}
|
||||
}
|
||||
|
||||
func getRoute(req *http.Request) (*Route, map[string]string) {
|
||||
vars := make(map[string]string)
|
||||
for _, route := range routes {
|
||||
if route.urlRegex.MatchString(req.URL.Path) {
|
||||
matches := route.urlRegex.FindStringSubmatchIndex(req.URL.Path)
|
||||
for i, key := range route.urlRegex.SubexpNames()[1:] {
|
||||
vars[key] = req.URL.Path[matches[i*2+2]:matches[i*2+3]]
|
||||
}
|
||||
return &route, vars
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
141
server/server.go
@@ -1,141 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/nkanaev/yarr/storage"
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
Addr string
|
||||
db *storage.Storage
|
||||
log *log.Logger
|
||||
feedQueue chan storage.Feed
|
||||
queueSize *int32
|
||||
}
|
||||
|
||||
func New(db *storage.Storage, logger *log.Logger, addr string) *Handler {
|
||||
queueSize := int32(0)
|
||||
return &Handler{
|
||||
db: db,
|
||||
log: logger,
|
||||
feedQueue: make(chan storage.Feed, 3000),
|
||||
queueSize: &queueSize,
|
||||
Addr: addr,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) Start() {
|
||||
h.startJobs()
|
||||
s := &http.Server{Addr: h.Addr, Handler: h}
|
||||
err := s.ListenAndServe()
|
||||
if err != http.ErrServerClosed {
|
||||
h.log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (h Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
route, vars := getRoute(req)
|
||||
if route == nil {
|
||||
rw.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
ctx := context.WithValue(req.Context(), ctxHandler, &h)
|
||||
ctx = context.WithValue(ctx, ctxVars, vars)
|
||||
route.handler(rw, req.WithContext(ctx))
|
||||
}
|
||||
|
||||
func (h *Handler) startJobs() {
|
||||
delTicker := time.NewTicker(time.Hour * 24)
|
||||
|
||||
syncSearchChannel := make(chan bool, 10)
|
||||
var syncSearchTimer *time.Timer // TODO: should this be atomic?
|
||||
|
||||
syncSearch := func() {
|
||||
if syncSearchTimer == nil {
|
||||
syncSearchTimer = time.AfterFunc(time.Second * 2, func() {
|
||||
syncSearchChannel <- true
|
||||
})
|
||||
} else {
|
||||
syncSearchTimer.Reset(time.Second * 2)
|
||||
}
|
||||
}
|
||||
|
||||
worker := func() {
|
||||
for {
|
||||
select {
|
||||
case feed := <-h.feedQueue:
|
||||
items, err := listItems(feed)
|
||||
atomic.AddInt32(h.queueSize, -1)
|
||||
if err != nil {
|
||||
h.log.Printf("Failed to fetch %s (%d): %s", feed.FeedLink, feed.Id, err)
|
||||
continue
|
||||
}
|
||||
h.db.CreateItems(items)
|
||||
syncSearch()
|
||||
if !feed.HasIcon {
|
||||
icon, err := findFavicon(feed.Link, feed.FeedLink)
|
||||
if icon != nil {
|
||||
h.db.UpdateFeedIcon(feed.Id, icon)
|
||||
}
|
||||
if err != nil {
|
||||
h.log.Printf("Failed to search favicon for %s (%s): %s", feed.Link, feed.FeedLink, err)
|
||||
}
|
||||
}
|
||||
case <- delTicker.C:
|
||||
h.db.DeleteOldItems()
|
||||
case <- syncSearchChannel:
|
||||
h.db.SyncSearch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
num := runtime.NumCPU() - 1
|
||||
if num < 1 {
|
||||
num = 1
|
||||
}
|
||||
for i := 0; i < num; i++ {
|
||||
go worker()
|
||||
}
|
||||
go h.db.DeleteOldItems()
|
||||
go h.db.SyncSearch()
|
||||
//h.fetchAllFeeds()
|
||||
}
|
||||
|
||||
func (h *Handler) fetchAllFeeds() {
|
||||
for _, feed := range h.db.ListFeeds() {
|
||||
h.fetchFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) fetchFeed(feed storage.Feed) {
|
||||
atomic.AddInt32(h.queueSize, 1)
|
||||
h.feedQueue <- feed
|
||||
}
|
||||
|
||||
func Vars(req *http.Request) map[string]string {
|
||||
if rv := req.Context().Value(ctxVars); rv != nil {
|
||||
return rv.(map[string]string)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func db(req *http.Request) *storage.Storage {
|
||||
if h := handler(req); h != nil {
|
||||
return h.db
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func handler(req *http.Request) *Handler {
|
||||
return req.Context().Value(ctxHandler).(*Handler)
|
||||
}
|
||||
|
||||
const (
|
||||
ctxVars = 2
|
||||
ctxHandler = 3
|
||||
)
|
||||
62
src/assets/assets.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package assets
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
type assetsfs struct {
|
||||
embedded *embed.FS
|
||||
templates map[string]*template.Template
|
||||
}
|
||||
|
||||
var FS assetsfs
|
||||
|
||||
func (afs assetsfs) Open(name string) (fs.File, error) {
|
||||
if afs.embedded != nil {
|
||||
return afs.embedded.Open(name)
|
||||
}
|
||||
return os.DirFS("src/assets").Open(name)
|
||||
}
|
||||
|
||||
func Template(path string) *template.Template {
|
||||
var tmpl *template.Template
|
||||
tmpl, found := FS.templates[path]
|
||||
if !found {
|
||||
tmpl = template.Must(template.New(path).Delims("{%", "%}").Funcs(template.FuncMap{
|
||||
"inline": func(svg string) template.HTML {
|
||||
svgfile, err := FS.Open("graphicarts/" + svg)
|
||||
// should never happen
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer svgfile.Close()
|
||||
|
||||
content, err := ioutil.ReadAll(svgfile)
|
||||
// should never happen
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return template.HTML(content)
|
||||
},
|
||||
}).ParseFS(FS, path))
|
||||
if FS.embedded != nil {
|
||||
FS.templates[path] = tmpl
|
||||
}
|
||||
}
|
||||
return tmpl
|
||||
}
|
||||
|
||||
func Render(path string, writer io.Writer, data interface{}) {
|
||||
tmpl := Template(path)
|
||||
tmpl.Execute(writer, data)
|
||||
}
|
||||
|
||||
func init() {
|
||||
FS.templates = make(map[string]*template.Template)
|
||||
}
|
||||
17
src/assets/assetsfs.go
Normal file
@@ -0,0 +1,17 @@
|
||||
//go:build release
|
||||
// +build release
|
||||
|
||||
package assets
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed *.html
|
||||
//go:embed graphicarts
|
||||
//go:embed javascripts
|
||||
//go:embed stylesheets
|
||||
//go:embed manifest.json
|
||||
var embedded embed.FS
|
||||
|
||||
func init() {
|
||||
FS.embedded = &embedded
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-more-vertical"><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-alert-circle"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
|
||||
|
Before Width: | Height: | Size: 341 B After Width: | Height: | Size: 356 B |
|
Before Width: | Height: | Size: 345 B After Width: | Height: | Size: 345 B |
BIN
src/assets/graphicarts/android/android-launchericon-144-144.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
src/assets/graphicarts/android/android-launchericon-192-192.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
src/assets/graphicarts/android/android-launchericon-48-48.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/assets/graphicarts/android/android-launchericon-512-512.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
src/assets/graphicarts/android/android-launchericon-72-72.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src/assets/graphicarts/android/android-launchericon-96-96.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 353 B After Width: | Height: | Size: 353 B |
|
Before Width: | Height: | Size: 339 B After Width: | Height: | Size: 339 B |
|
Before Width: | Height: | Size: 262 B After Width: | Height: | Size: 262 B |
|
Before Width: | Height: | Size: 270 B After Width: | Height: | Size: 270 B |
|
Before Width: | Height: | Size: 270 B After Width: | Height: | Size: 270 B |
|
Before Width: | Height: | Size: 267 B After Width: | Height: | Size: 267 B |
|
Before Width: | Height: | Size: 258 B After Width: | Height: | Size: 258 B |
|
Before Width: | Height: | Size: 370 B After Width: | Height: | Size: 370 B |
|
Before Width: | Height: | Size: 365 B After Width: | Height: | Size: 365 B |
|
Before Width: | Height: | Size: 388 B After Width: | Height: | Size: 388 B |
BIN
src/assets/graphicarts/favicon.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
9
src/assets/graphicarts/favicon.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-anchor-favicon">
|
||||
<circle cx="12" cy="5" r="3" stroke-width="4" stroke="#ffffff"></circle>
|
||||
<line x1="12" y1="22" x2="12" y2="8" stroke-width="4" stroke="#ffffff"></line>
|
||||
<path d="M5 12H2a10 10 0 0 0 20 0h-3" stroke-width="4" stroke="#ffffff"></path>
|
||||
|
||||
<circle cx="12" cy="5" r="3"></circle>
|
||||
<line x1="12" y1="22" x2="12" y2="8"></line>
|
||||
<path d="M5 12H2a10 10 0 0 0 20 0h-3"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 603 B |
1
src/assets/graphicarts/folder-minus.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-folder-minus"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path><line x1="9" y1="14" x2="15" y2="14"></line></svg>
|
||||
|
After Width: | Height: | Size: 361 B |
1
src/assets/graphicarts/folder-plus.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-folder-plus"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path><line x1="12" y1="11" x2="12" y2="17"></line><line x1="9" y1="14" x2="15" y2="14"></line></svg>
|
||||
|
After Width: | Height: | Size: 405 B |
|
Before Width: | Height: | Size: 311 B After Width: | Height: | Size: 311 B |
1
src/assets/graphicarts/globe.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-globe"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>
|
||||
|
After Width: | Height: | Size: 409 B |
1
src/assets/graphicarts/help-circle.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-help-circle"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
|
||||
|
After Width: | Height: | Size: 365 B |
BIN
src/assets/graphicarts/ios/100.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
src/assets/graphicarts/ios/1024.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
src/assets/graphicarts/ios/114.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
src/assets/graphicarts/ios/120.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
src/assets/graphicarts/ios/128.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
src/assets/graphicarts/ios/144.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
src/assets/graphicarts/ios/152.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
src/assets/graphicarts/ios/16.png
Normal file
|
After Width: | Height: | Size: 515 B |
BIN
src/assets/graphicarts/ios/167.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
src/assets/graphicarts/ios/180.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
src/assets/graphicarts/ios/192.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
src/assets/graphicarts/ios/20.png
Normal file
|
After Width: | Height: | Size: 699 B |
BIN
src/assets/graphicarts/ios/256.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/assets/graphicarts/ios/29.png
Normal file
|
After Width: | Height: | Size: 939 B |
BIN
src/assets/graphicarts/ios/32.png
Normal file
|
After Width: | Height: | Size: 1010 B |
BIN
src/assets/graphicarts/ios/40.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/assets/graphicarts/ios/50.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src/assets/graphicarts/ios/512.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
src/assets/graphicarts/ios/57.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/assets/graphicarts/ios/58.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/assets/graphicarts/ios/60.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |