25 Commits
v1.0 ... v1.1

Author SHA1 Message Date
Nazar Kanaev
bcab24ebfa v1.1 2020-10-05 22:27:09 +01:00
Nazar Kanaev
dd058e1637 autogenerate version 2020-10-05 21:59:53 +01:00
nkanaev
63f624251d Update readme.md 2020-10-05 16:16:04 +01:00
Nazar Kanaev
05ee99b4d9 update build.yml 2020-10-05 15:39:19 +01:00
Nazar Kanaev
d5cc9149e3 do not refresh items after feed is deselected 2020-10-03 21:33:35 +01:00
Nazar Kanaev
eb2029132c oopsies 2020-10-03 20:54:51 +01:00
Nazar Kanaev
492e77fd25 done 2020-10-03 13:01:22 +01:00
Nazar Kanaev
b8aa15d554 tablet & mobile layouts 2020-10-03 12:57:30 +01:00
Nazar Kanaev
ee0b440b7b bug fix 2020-10-02 22:14:01 +01:00
Nazar Kanaev
72da9df5ac space 2020-10-02 21:15:55 +01:00
Nazar Kanaev
9872bf84f0 truncate choice text 2020-10-02 20:09:03 +01:00
Nazar Kanaev
6222761dd3 fix favicon fetch bug 2020-10-02 16:49:34 +01:00
Nazar Kanaev
71cc8929ad remove log 2020-10-02 15:18:57 +01:00
Nazar Kanaev
8007853a9a tweak timeouts 2020-10-02 15:17:59 +01:00
Nazar Kanaev
ffc506371c default build 2020-10-02 12:38:33 +01:00
Nazar Kanaev
00fed5e0cf show addr in logs 2020-10-02 12:37:50 +01:00
Nazar Kanaev
6937c349f0 rename assets go file 2020-10-01 21:57:34 +01:00
Nazar Kanaev
bac136603b move icons to platform package 2020-10-01 21:53:48 +01:00
Nazar Kanaev
693b5bcb8d handle server error 2020-09-25 19:23:38 +01:00
Nazar Kanaev
9c7d95f632 tweak 2020-09-25 11:59:08 +01:00
Nazar Kanaev
b8adb3fa2f fluid width 2020-09-25 11:32:15 +01:00
nkanaev
5652b240dc Update hacking.md 2020-09-25 00:05:57 +01:00
nkanaev
c1c0ffab01 Update hacking.md 2020-09-24 23:55:23 +01:00
Nazar Kanaev
349a8980f2 hacking 2020-09-24 23:54:01 +01:00
nkanaev
0bd117804d Update build.yml 2020-09-24 23:28:38 +01:00
19 changed files with 353 additions and 139 deletions

View File

@@ -2,7 +2,7 @@ name: build
on: on:
push: push:
branches: [master] tags: [v*]
jobs: jobs:
build_macos: build_macos:
@@ -11,23 +11,42 @@ jobs:
steps: steps:
- {name: "Checkout", uses: actions/checkout@v2} - {name: "Checkout", uses: actions/checkout@v2}
- {name: "Checkout gofeed", uses: actions/checkout@v2, with: {repository: nkanaev/gofeed, path: gofeed}} - {name: "Checkout gofeed", uses: actions/checkout@v2, with: {repository: nkanaev/gofeed, path: gofeed}}
- name: Cache Go Modules
uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: "Build" - name: "Build"
run: | run: make build_macos
go version - name: Upload
make build_macos uses: actions/upload-artifact@v2
cd _output/macos && zip -r yarr-macos.zip yarr.app with:
- {name: "Upload", uses: actions/upload-artifact@v2, with: {name: macos, path: _output/macos/yarr-macos.zip}} name: macos
path: _output/macos/yarr.app
build_windows: build_windows:
name: Build for Windows name: Build for Windows
runs-on: windows-2019 runs-on: windows-2019
steps: steps:
- {name: "Checkout", uses: actions/checkout@v2} - {name: "Checkout", uses: actions/checkout@v2}
- {name: "Checkout gofeed", uses: actions/checkout@v2, with: {repository: nkanaev/gofeed, path: gofeed}} - {name: "Checkout gofeed", uses: actions/checkout@v2, with: {repository: nkanaev/gofeed, path: gofeed}}
- name: Cache Go Modules
uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: "Build" - name: "Build"
run: | run: make build_windows
go version - name: Upload
make build_windows uses: actions/upload-artifact@v2
- {name: "Upload", uses: actions/upload-artifact@v2, with: {name: windows, path: _output/windows/yarr.exe}} with:
name: windows
path: _output/windows/yarr.exe
build_linux: build_linux:
name: Build for Linux name: Build for Linux
runs-on: ubuntu-18.04 runs-on: ubuntu-18.04
@@ -35,9 +54,73 @@ jobs:
- {name: "Checkout", uses: actions/checkout@v2} - {name: "Checkout", uses: actions/checkout@v2}
- {name: "Checkout gofeed", uses: actions/checkout@v2, with: {repository: nkanaev/gofeed, path: gofeed}} - {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: "Setup Go", uses: actions/setup-go@v2, with: {go-version: '^1.14'}}
- name: Cache Go Modules
uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: "Build" - name: "Build"
run: make build_linux
- name: Upload
uses: actions/upload-artifact@v2
with:
name: linux
path: _output/linux/yarr
create_release:
name: Create Release
runs-on: ubuntu-latest
needs: [build_macos, build_windows, build_linux]
steps:
- name: Create Release
uses: actions/create-release@v1
id: create_release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
draft: true
prerelease: true
- name: Download Artifacts
uses: actions/download-artifact@v2
with:
path: .
- name: Preparation
run: | run: |
go version ls -R
make build_linux chmod u+x macos/Contents/MacOS/yarr
cd _output/linux && zip -r yarr-linux.zip yarr chmod u+x linux/yarr
- {name: "Upload", uses: actions/upload-artifact@v2, with: {name: linux, path: _output/linux/yarr-linux.zip}}
mv macos yarr.app && zip -r yarr-macos.zip yarr.app
mv windows/yarr.exe . && zip yarr-windows.zip yarr.exe
mv linux/yarr . && zip yarr-linux.zip yarr
- name: Upload MacOS
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./yarr-macos.zip
asset_name: yarr-${{ github.ref }}-macos64.zip
asset_content_type: application/zip
- name: Upload Windows
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./yarr-windows.zip
asset_name: yarr-${{ github.ref }}-windows32.zip
asset_content_type: application/zip
- name: Upload Linux
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./yarr-linux.zip
asset_name: yarr-${{ github.ref }}-linux32.zip
asset_content_type: application/zip

3
.gitignore vendored
View File

@@ -1,6 +1,7 @@
/server/assets_bundle.go /server/assets.go
/gofeed /gofeed
/_output /_output
/yarr /yarr
*.db *.db
*.syso *.syso
versioninfo.rc

View File

@@ -1,26 +0,0 @@
1 VERSIONINFO
FILEVERSION 1,0,0,0
PRODUCTVERSION 1,0,0,0
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "080904E4"
BEGIN
VALUE "CompanyName", "Old MacDonald's Farm"
VALUE "FileDescription", "Yet another RSS reader"
VALUE "FileVersion", "1.0"
VALUE "InternalName", "yarr"
VALUE "LegalCopyright", "nkanaev"
VALUE "OriginalFilename", "yarr.exe"
VALUE "ProductName", "yarr"
VALUE "ProductVersion", "1.0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x809, 1252
END
END
1 ICON "icon.ico"

View 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-chevron-left"><polyline points="15 18 9 12 15 6"></polyline></svg>

After

Width:  |  Height:  |  Size: 270 B

View File

@@ -6,11 +6,12 @@
<link rel="stylesheet" href="./static/stylesheets/bootstrap.min.css"> <link rel="stylesheet" href="./static/stylesheets/bootstrap.min.css">
<link rel="stylesheet" href="./static/stylesheets/app.css"> <link rel="stylesheet" href="./static/stylesheets/app.css">
<link rel="icon shortcut" href="./static/graphicarts/anchor.png"> <link rel="icon shortcut" href="./static/graphicarts/anchor.png">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
</head> </head>
<body class="theme-light"> <body class="theme-light">
<div class="wrapper d-flex vh-100" id="app" v-cloak> <div id="app" class="d-flex" :class="{'feed-selected': feedSelected !== null, 'item-selected': itemSelected !== null}" v-cloak>
<!-- feed list --> <!-- feed list -->
<div class="vh-100 position-relative d-flex flex-column border-right flex-shrink-0" :style="{width: feedListWidth+'px'}"> <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> <drag :width="feedListWidth" @resize="resizeFeedList"></drag>
<div class="p-2 toolbar d-flex align-items-center"> <div class="p-2 toolbar d-flex align-items-center">
<div class="icon mx-2">{% inline "anchor.svg" %}</div> <div class="icon mx-2">{% inline "anchor.svg" %}</div>
@@ -134,9 +135,14 @@
</div> </div>
</div> </div>
<!-- item list --> <!-- item list -->
<div class="vh-100 position-relative d-flex flex-column border-right flex-shrink-0" :style="{width: itemListWidth+'px'}"> <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> <drag :width="itemListWidth" @resize="resizeItemList"></drag>
<div class="px-2 toolbar d-flex align-items-center"> <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"> <div class="input-icon flex-grow-1">
<span class="icon">{% inline "search.svg" %}</span> <span class="icon">{% inline "search.svg" %}</span>
<input class="d-block toolbar-search" type="" v-model="itemSearch"> <input class="d-block toolbar-search" type="" v-model="itemSearch">
@@ -170,7 +176,7 @@
</div> </div>
</div> </div>
<!-- item show --> <!-- item show -->
<div class="vh-100 d-flex flex-column w-100" style="min-width: 0;"> <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"> <div class="toolbar px-2 d-flex align-items-center" v-if="itemSelected">
<button class="toolbar-item" <button class="toolbar-item"
@click="toggleItemStarred(itemSelectedDetails)" @click="toggleItemStarred(itemSelectedDetails)"
@@ -278,8 +284,8 @@
<label class="selectgroup" v-for="choice in feedNewChoice"> <label class="selectgroup" v-for="choice in feedNewChoice">
<input type="radio" name="feedToAdd" :value="choice.url" v-model="feedNewChoiceSelected"> <input type="radio" name="feedToAdd" :value="choice.url" v-model="feedNewChoiceSelected">
<div class="selectgroup-label"> <div class="selectgroup-label">
<div>{{ choice.title }}</div> <div class="text-truncate">{{ choice.title }}</div>
<div :class="{light: choice.title}">{{ choice.url }}</div> <div class="text-truncate" :class="{light: choice.title}">{{ choice.url }}</div>
</div> </div>
</label> </label>
</div> </div>

View File

@@ -126,11 +126,11 @@ var vm = new Vue({
}, },
data: function() { data: function() {
return { return {
'filterSelected': null, 'filterSelected': undefined,
'folders': [], 'folders': [],
'feeds': [], 'feeds': [],
'feedSelected': null, 'feedSelected': undefined,
'feedListWidth': null, 'feedListWidth': undefined,
'feedNewChoice': [], 'feedNewChoice': [],
'feedNewChoiceSelected': '', 'feedNewChoiceSelected': '',
'items': [], 'items': [],
@@ -142,8 +142,8 @@ var vm = new Vue({
'itemSelectedDetails': {}, 'itemSelectedDetails': {},
'itemSelectedReadability': '', 'itemSelectedReadability': '',
'itemSearch': '', 'itemSearch': '',
'itemSortNewestFirst': null, 'itemSortNewestFirst': undefined,
'itemListWidth': null, 'itemListWidth': undefined,
'filteredFeedStats': {}, 'filteredFeedStats': {},
'filteredFolderStats': {}, 'filteredFolderStats': {},
@@ -229,13 +229,13 @@ var vm = new Vue({
}, 500), }, 500),
}, },
'filterSelected': function(newVal, oldVal) { 'filterSelected': function(newVal, oldVal) {
if (oldVal === null) return // do nothing, initial setup if (oldVal === undefined) return // do nothing, initial setup
api.settings.update({filter: newVal}).then(this.refreshItems.bind(this)) api.settings.update({filter: newVal}).then(this.refreshItems.bind(this))
this.itemSelected = null this.itemSelected = null
this.computeStats() this.computeStats()
}, },
'feedSelected': function(newVal, oldVal) { 'feedSelected': function(newVal, oldVal) {
if (oldVal === null) return // do nothing, initial setup if (oldVal === undefined) return // do nothing, initial setup
api.settings.update({feed: newVal}).then(this.refreshItems.bind(this)) api.settings.update({feed: newVal}).then(this.refreshItems.bind(this))
this.itemSelected = null this.itemSelected = null
if (this.$refs.itemlist) this.$refs.itemlist.scrollTop = 0 if (this.$refs.itemlist) this.$refs.itemlist.scrollTop = 0
@@ -259,15 +259,15 @@ var vm = new Vue({
this.refreshItems() this.refreshItems()
}, 500), }, 500),
'itemSortNewestFirst': function(newVal, oldVal) { 'itemSortNewestFirst': function(newVal, oldVal) {
if (oldVal === null) return if (oldVal === undefined) return // do nothing, initial setup
api.settings.update({sort_newest_first: newVal}).then(this.refreshItems.bind(this)) api.settings.update({sort_newest_first: newVal}).then(this.refreshItems.bind(this))
}, },
'feedListWidth': debounce(function(newVal, oldVal) { 'feedListWidth': debounce(function(newVal, oldVal) {
if (oldVal === null) return if (oldVal === undefined) return // do nothing, initial setup
api.settings.update({feed_list_width: newVal}) api.settings.update({feed_list_width: newVal})
}, 1000), }, 1000),
'itemListWidth': debounce(function(newVal, oldVal) { 'itemListWidth': debounce(function(newVal, oldVal) {
if (oldVal === null) return if (oldVal === undefined) return // do nothing, initial setup
api.settings.update({item_list_width: newVal}) api.settings.update({item_list_width: newVal})
}, 1000), }, 1000),
}, },
@@ -318,9 +318,14 @@ var vm = new Vue({
}) })
}, },
refreshItems: function() { refreshItems: function() {
if (this.feedSelected === null) {
vm.items = []
vm.itemsPage = {'cur': 1, 'num': 1}
return
}
var query = this.getItemsQuery() var query = this.getItemsQuery()
this.loading.items = true this.loading.items = true
api.items.list(query).then(function(data) { return api.items.list(query).then(function(data) {
vm.items = data.list vm.items = data.list
vm.itemsPage = data.page vm.itemsPage = data.page
vm.loading.items = false vm.loading.items = false
@@ -420,10 +425,17 @@ var vm = new Vue({
deleteFeed: function(feed) { deleteFeed: function(feed) {
if (confirm('Are you sure you want to delete ' + feed.title + '?')) { if (confirm('Are you sure you want to delete ' + feed.title + '?')) {
api.feeds.delete(feed.id).then(function() { api.feeds.delete(feed.id).then(function() {
if (vm.feedSelected === 'feed:'+feed.id) { // note: if item list contains delete feed's entries, refresh it first.
vm.items = [] for (var i = 0; i < vm.items.length; i++) {
vm.feedSelected = '' if (vm.items[i].feed_id == feed.id) {
vm.refreshItems().then(function() {
vm.refreshStats()
vm.refreshFeeds()
})
return
} }
}
vm.refreshStats() vm.refreshStats()
vm.refreshFeeds() vm.refreshFeeds()
}) })

View File

@@ -6,12 +6,6 @@ body {
font-size: 15px !important; font-size: 15px !important;
} }
.wrapper {
max-width: 1440px;
margin: 0 auto;
overflow-x: hidden;
}
/* bootstrap customizations */ /* bootstrap customizations */
.btn-link { .btn-link {
@@ -418,6 +412,7 @@ select.form-control:not([multiple]):not([size]) {
.content pre { .content pre {
color: inherit; color: inherit;
overflow-x: scroll;
} }
.content a { .content a {
@@ -430,10 +425,6 @@ select.form-control:not([multiple]):not([size]) {
padding-left: 1rem; padding-left: 1rem;
} }
.content pre {
overflow-x: scroll;
}
.content h1 { .content h1 {
font-size: 1.8rem; font-size: 1.8rem;
} }
@@ -519,3 +510,71 @@ a,
opacity: 0; opacity: 0;
margin: 0 !important; margin: 0 !important;
} }
/* responsive layout
tablet:
none selected: show feed list & item list
feed selected: show feed list & item list
item selected: show item
mobile:
none selected: show feed list
feed selected: show item list
item selected: show item
*/
@media (min-width: 768px) and (max-width: 991.98px) {
#app #col-feed-list {
width: 35% !important;
}
#app #col-item-list {
width: 65% !important;
border-right-width: 0 !important;
}
#app #col-item {
display: none !important;
}
#app.item-selected #col-feed-list {
display: none !important;
}
#app.item-selected #col-item-list {
display: none !important;
}
#app.item-selected #col-item {
display: flex !important;
}
}
@media (max-width: 767.98px) {
#app #col-feed-list {
width: 100% !important;
border-right-width: 0 !important;
}
#app #col-item-list {
width: 100% !important;
display: none !important;
border-right-width: 0 !important;
}
#app #col-item {
width: 100% !important;
display: none !important;
}
#app.feed-selected #col-feed-list {
display: none !important;
}
#app.feed-selected #col-item-list {
display: flex !important;
}
#app.item-selected #col-feed-list {
display: none !important;
}
#app.item-selected #col-item-list {
display: none !important;
}
#app.item-selected #col-item {
display: flex !important;
}
}

37
hacking.md Normal file
View File

@@ -0,0 +1,37 @@
# 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!

View File

@@ -11,7 +11,7 @@ import (
"path/filepath" "path/filepath"
) )
var Version string = "v0.0" var Version string = "0.0"
var GitHash string = "unknown" var GitHash string = "unknown"
func main() { func main() {
@@ -23,7 +23,7 @@ func main() {
flag.Parse() flag.Parse()
if ver { if ver {
fmt.Printf("%s (%s)\n", Version, GitHash) fmt.Printf("v%s (%s)\n", Version, GitHash)
return return
} }
@@ -48,5 +48,6 @@ func main() {
} }
srv := server.New(db, logger, addr) srv := server.New(db, logger, addr)
logger.Printf("starting server at http://%s", addr)
platform.Start(srv) platform.Start(srv)
} }

View File

@@ -1,4 +1,4 @@
VERSION=v1.0 VERSION=1.1
GITHASH=$(shell git rev-parse --short=8 HEAD) GITHASH=$(shell git rev-parse --short=8 HEAD)
ASSETS = assets/javascripts/* assets/stylesheets/* assets/graphicarts/* assets/index.html ASSETS = assets/javascripts/* assets/stylesheets/* assets/graphicarts/* assets/index.html
@@ -9,10 +9,14 @@ GO_LDFLAGS := $(GO_LDFLAGS) -X 'main.Version=$(VERSION)' -X 'main.GitHash=$(GITH
default: bundle default: bundle
server/assets_bundle.go: $(ASSETS) server/assets.go: $(ASSETS)
go run scripts/bundle_assets.go >/dev/null go run scripts/bundle_assets.go >/dev/null
bundle: server/assets_bundle.go bundle: server/assets.go
build_default: bundle
mkdir -p _output
go build -tags "sqlite_foreign_keys release" -ldflags="$(GO_LDFLAGS)" -o _output/yarr main.go
build_macos: bundle build_macos: bundle
set GOOS=darwin set GOOS=darwin
@@ -20,7 +24,7 @@ build_macos: bundle
mkdir -p _output/macos mkdir -p _output/macos
go build -tags "sqlite_foreign_keys release macos" -ldflags="$(GO_LDFLAGS)" -o _output/macos/yarr main.go 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 cp artwork/icon.png _output/macos/icon.png
go run scripts/package_macos.go _output/macos go run scripts/package_macos.go -outdir _output/macos -version "$(VERSION)"
build_linux: bundle build_linux: bundle
set GOOS=linux set GOOS=linux
@@ -32,5 +36,6 @@ build_windows: bundle
set GOOS=windows set GOOS=windows
set GOARCH=386 set GOARCH=386
mkdir -p _output/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 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 build -tags "sqlite_foreign_keys release windows" -ldflags="$(GO_LDFLAGS) -H windowsgui" -o _output/windows/yarr.exe main.go

View File

@@ -10,7 +10,7 @@ import (
func Start(s *server.Handler) { func Start(s *server.Handler) {
systrayOnReady := func() { systrayOnReady := func() {
systray.SetIcon(server.Icon) systray.SetIcon(Icon)
menuOpen := systray.AddMenuItem("Open", "") menuOpen := systray.AddMenuItem("Open", "")
systray.AddSeparator() systray.AddSeparator()

View File

@@ -1,8 +1,8 @@
// +build !windows // +build macos
// File generated by 2goarray v0.1.0 (http://github.com/cratonica/2goarray) // File generated by 2goarray v0.1.0 (http://github.com/cratonica/2goarray)
package server package platform
var Icon []byte = []byte{ var Icon []byte = []byte{
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,

View File

@@ -2,7 +2,7 @@
// File generated by 2goarray v0.1.0 (http://github.com/cratonica/2goarray) // File generated by 2goarray v0.1.0 (http://github.com/cratonica/2goarray)
package server package platform
var Icon []byte = []byte{ var Icon []byte = []byte{
0x00, 0x00, 0x01, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,

View File

@@ -1,4 +1,4 @@
# yarr (beta) # yarr
yet another rss reader. yet another rss reader.
@@ -8,36 +8,10 @@ yet another rss reader.
The goal of the project is to provide a desktop application accessible via web browser. 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. Longer-term plans include a self-hosted solution for individuals.
There are plans to add support for mobile & tablet resolutions.
Support for 3rd-party applications (via Fever API) is being considered. Support for 3rd-party applications (via Fever API) is being considered.
## build [download](https://github.com/nkanaev/yarr/releases/latest)
Install `Go >= 1.14` and `gcc`, then run:
```sh
git clone https://github.com/nkanaev/yarr.git
git clone https://github.com/nkanaev/gofeed.git
mv gofeed yarr
cd yarr && make build_macos
```
## plans
- test across 3 platforms (macos, linux, windows)
- prebuilt binaries
- GUI-less mode (no tray icon)
- feeds health checker
- mobile & tablet layout
- parameters (`--[no]-gui`, `--addr`, ...)
- Fever API support
- keyboard navigation
## credits ## credits
[Feather](http://feathericons.com/) for icons. [Feather](http://feathericons.com/) for icons.
## code of conduct
Be excellent to each other. Party on, dudes!

View File

@@ -92,5 +92,5 @@ func main() {
var buf bytes.Buffer var buf bytes.Buffer
template := template.Must(template.New("code").Parse(code_template)) template := template.Must(template.New("code").Parse(code_template))
template.Execute(&buf, assets) template.Execute(&buf, assets)
ioutil.WriteFile("server/assets_bundle.go", buf.Bytes(), 0644) ioutil.WriteFile("server/assets.go", buf.Bytes(), 0644)
} }

View File

@@ -0,0 +1,48 @@
package main
import (
"io/ioutil"
"flag"
"strings"
)
var rsrc = `1 VERSIONINFO
FILEVERSION {VERSION_COMMA},0,0
PRODUCTVERSION {VERSION_COMMA},0,0
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "080904E4"
BEGIN
VALUE "CompanyName", "Old MacDonald's Farm"
VALUE "FileDescription", "Yet another RSS reader"
VALUE "FileVersion", "{VERSION}"
VALUE "InternalName", "yarr"
VALUE "LegalCopyright", "nkanaev"
VALUE "OriginalFilename", "yarr.exe"
VALUE "ProductName", "yarr"
VALUE "ProductVersion", "{VERSION}"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x809, 1252
END
END
1 ICON "icon.ico"
`
func main() {
var version, outfile string
flag.StringVar(&version, "version", "0.0", "")
flag.StringVar(&outfile, "outfile", "versioninfo.rc", "")
flag.Parse()
version_comma := strings.ReplaceAll(version, ".", ",")
out := strings.ReplaceAll(rsrc, "{VERSION}", version)
out = strings.ReplaceAll(out, "{VERSION_COMMA}", version_comma)
ioutil.WriteFile(outfile, []byte(out), 0644)
}

View File

@@ -1,13 +1,15 @@
package main package main
import ( import (
"os" "flag"
"path"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os/exec"
"strconv"
"log" "log"
"os"
"os/exec"
"path"
"strconv"
"strings"
) )
var plist = `<?xml version="1.0" encoding="UTF-8"?> var plist = `<?xml version="1.0" encoding="UTF-8"?>
@@ -21,7 +23,7 @@ var plist = `<?xml version="1.0" encoding="UTF-8"?>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>nkanaev.yarr</string> <string>nkanaev.yarr</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.0</string> <string>VERSION</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
@@ -35,11 +37,6 @@ var plist = `<?xml version="1.0" encoding="UTF-8"?>
<key>NSHighResolutionCapable</key> <key>NSHighResolutionCapable</key>
<string>True</string> <string>True</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>10.13</string> <string>10.13</string>
<key>LSUIElement</key> <key>LSUIElement</key>
@@ -59,7 +56,11 @@ func run(cmd ...string) {
} }
func main() { func main() {
outdir := os.Args[1] var version, outdir string
flag.StringVar(&version, "version", "0.0", "")
flag.StringVar(&outdir, "outdir", "", "")
flag.Parse()
outfile := "yarr" outfile := "yarr"
binDir := path.Join(outdir, "yarr.app", "Contents/MacOS") binDir := path.Join(outdir, "yarr.app", "Contents/MacOS")
@@ -74,7 +75,7 @@ func main() {
f, _ := ioutil.ReadFile(path.Join(outdir, outfile)) f, _ := ioutil.ReadFile(path.Join(outdir, outfile))
ioutil.WriteFile(path.Join(binDir, outfile), f, 0755) ioutil.WriteFile(path.Join(binDir, outfile), f, 0755)
ioutil.WriteFile(plistFile, []byte(plist), 0644) ioutil.WriteFile(plistFile, []byte(strings.Replace(plist, "VERSION", version, 1)), 0644)
ioutil.WriteFile(pkginfoFile, []byte("APPL????"), 0644) ioutil.WriteFile(pkginfoFile, []byte("APPL????"), 0644)
iconFile := path.Join(outdir, "icon.png") iconFile := path.Join(outdir, "icon.png")

View File

@@ -8,6 +8,7 @@ import (
"github.com/mmcdole/gofeed" "github.com/mmcdole/gofeed"
"github.com/nkanaev/yarr/storage" "github.com/nkanaev/yarr/storage"
"io/ioutil" "io/ioutil"
"net"
"net/http" "net/http"
"net/url" "net/url"
"time" "time"
@@ -157,13 +158,16 @@ func findFavicon(websiteUrl, feedUrl string) (*[]byte, error) {
} }
if len(websiteUrl) != 0 { if len(websiteUrl) != 0 {
base, err := url.Parse(websiteUrl)
if err != nil {
return nil, err
}
res, err := defaultClient.get(websiteUrl) res, err := defaultClient.get(websiteUrl)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer res.Body.Close() defer res.Body.Close()
doc, err := goquery.NewDocumentFromReader(res.Body) doc, err := goquery.NewDocumentFromReader(res.Body)
base, err := url.Parse(websiteUrl)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -258,10 +262,16 @@ func listItems(f storage.Feed) ([]storage.Item, error) {
} }
func init() { func init() {
transport := http.DefaultTransport.(*http.Transport).Clone() transport := &http.Transport{
transport.DisableKeepAlives = true Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
}).DialContext,
DisableKeepAlives: true,
TLSHandshakeTimeout: time.Second * 10,
}
httpClient := &http.Client{ httpClient := &http.Client{
Timeout: time.Second * 5, Timeout: time.Second * 30,
Transport: transport, Transport: transport,
} }
defaultClient = &Client{ defaultClient = &Client{

View File

@@ -32,7 +32,10 @@ func New(db *storage.Storage, logger *log.Logger, addr string) *Handler {
func (h *Handler) Start() { func (h *Handler) Start() {
h.startJobs() h.startJobs()
s := &http.Server{Addr: h.Addr, Handler: h} s := &http.Server{Addr: h.Addr, Handler: h}
s.ListenAndServe() err := s.ListenAndServe()
if err != http.ErrServerClosed {
h.log.Fatal(err)
}
} }
func (h Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { func (h Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
@@ -75,13 +78,12 @@ func (h *Handler) startJobs() {
h.db.CreateItems(items) h.db.CreateItems(items)
syncSearch() syncSearch()
if !feed.HasIcon { if !feed.HasIcon {
h.log.Println("searching favicon for:", feed.Link, feed.FeedLink)
icon, err := findFavicon(feed.Link, feed.FeedLink) icon, err := findFavicon(feed.Link, feed.FeedLink)
if icon != nil { if icon != nil {
h.db.UpdateFeedIcon(feed.Id, icon) h.db.UpdateFeedIcon(feed.Id, icon)
} }
if err != nil { if err != nil {
h.log.Print(err) h.log.Printf("Failed to search favicon for %s (%s): %s", feed.Link, feed.FeedLink, err)
} }
} }
case <- delTicker.C: case <- delTicker.C: