mirror of
https://github.com/nkanaev/yarr.git
synced 2025-09-14 10:20:06 +00:00
Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
bcab24ebfa | ||
|
dd058e1637 | ||
|
63f624251d | ||
|
05ee99b4d9 | ||
|
d5cc9149e3 | ||
|
eb2029132c | ||
|
492e77fd25 | ||
|
b8aa15d554 | ||
|
ee0b440b7b | ||
|
72da9df5ac | ||
|
9872bf84f0 | ||
|
6222761dd3 | ||
|
71cc8929ad | ||
|
8007853a9a | ||
|
ffc506371c | ||
|
00fed5e0cf | ||
|
6937c349f0 | ||
|
bac136603b | ||
|
693b5bcb8d | ||
|
9c7d95f632 | ||
|
b8adb3fa2f | ||
|
5652b240dc | ||
|
c1c0ffab01 | ||
|
349a8980f2 | ||
|
0bd117804d |
111
.github/workflows/build.yml
vendored
111
.github/workflows/build.yml
vendored
@@ -2,7 +2,7 @@ name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
tags: [v*]
|
||||
|
||||
jobs:
|
||||
build_macos:
|
||||
@@ -11,23 +11,42 @@ jobs:
|
||||
steps:
|
||||
- {name: "Checkout", uses: actions/checkout@v2}
|
||||
- {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"
|
||||
run: |
|
||||
go version
|
||||
make build_macos
|
||||
cd _output/macos && zip -r yarr-macos.zip yarr.app
|
||||
- {name: "Upload", uses: actions/upload-artifact@v2, with: {name: macos, path: _output/macos/yarr-macos.zip}}
|
||||
run: make build_macos
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: macos
|
||||
path: _output/macos/yarr.app
|
||||
|
||||
build_windows:
|
||||
name: Build for Windows
|
||||
runs-on: windows-2019
|
||||
steps:
|
||||
- {name: "Checkout", uses: actions/checkout@v2}
|
||||
- {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"
|
||||
run: |
|
||||
go version
|
||||
make build_windows
|
||||
- {name: "Upload", uses: actions/upload-artifact@v2, with: {name: windows, path: _output/windows/yarr.exe}}
|
||||
run: make build_windows
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: windows
|
||||
path: _output/windows/yarr.exe
|
||||
|
||||
build_linux:
|
||||
name: Build for Linux
|
||||
runs-on: ubuntu-18.04
|
||||
@@ -35,9 +54,73 @@ jobs:
|
||||
- {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: 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"
|
||||
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: |
|
||||
go version
|
||||
make build_linux
|
||||
cd _output/linux && zip -r yarr-linux.zip yarr
|
||||
- {name: "Upload", uses: actions/upload-artifact@v2, with: {name: linux, path: _output/linux/yarr-linux.zip}}
|
||||
ls -R
|
||||
chmod u+x macos/Contents/MacOS/yarr
|
||||
chmod u+x linux/yarr
|
||||
|
||||
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
3
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
/server/assets_bundle.go
|
||||
/server/assets.go
|
||||
/gofeed
|
||||
/_output
|
||||
/yarr
|
||||
*.db
|
||||
*.syso
|
||||
versioninfo.rc
|
||||
|
@@ -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"
|
||||
|
1
assets/graphicarts/chevron-left.svg
Normal file
1
assets/graphicarts/chevron-left.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-chevron-left"><polyline points="15 18 9 12 15 6"></polyline></svg>
|
After Width: | Height: | Size: 270 B |
@@ -6,11 +6,12 @@
|
||||
<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 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 -->
|
||||
<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>
|
||||
<div class="p-2 toolbar d-flex align-items-center">
|
||||
<div class="icon mx-2">{% inline "anchor.svg" %}</div>
|
||||
@@ -92,7 +93,7 @@
|
||||
<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>
|
||||
<span class="counter text-right">{{ filteredTotalStats }}</span>
|
||||
</div>
|
||||
</label>
|
||||
<div v-for="folder in foldersWithFeeds">
|
||||
@@ -108,7 +109,7 @@
|
||||
{% 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>
|
||||
<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}">
|
||||
@@ -122,7 +123,7 @@
|
||||
<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>
|
||||
<span class="counter text-right">{{ filteredFeedStats[feed.id] || '' }}</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
@@ -130,13 +131,18 @@
|
||||
</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>
|
||||
<span class="text-truncate cursor-default noselect">Refreshing ({{ loading.feeds }} left)</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 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>
|
||||
<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">
|
||||
@@ -159,18 +165,18 @@
|
||||
<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}}
|
||||
{{ feedsById[item.feed_id].title }}
|
||||
</small>
|
||||
<small class="flex-shrink-0"><relative-time :val="item.date"/></small>
|
||||
</div>
|
||||
<div>{{item.title || 'untitled'}}</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 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">
|
||||
<button class="toolbar-item"
|
||||
@click="toggleItemStarred(itemSelectedDetails)"
|
||||
@@ -244,7 +250,7 @@
|
||||
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>
|
||||
<h1><b>{{ itemSelectedDetails.title }}</b></h1>
|
||||
<div class="text-muted">
|
||||
<div>{{ feedsById[itemSelectedDetails.feed_id].title }}</div>
|
||||
<time>{{ formatDate(itemSelectedDetails.date) }}</time>
|
||||
@@ -278,8 +284,8 @@
|
||||
<label class="selectgroup" v-for="choice in feedNewChoice">
|
||||
<input type="radio" name="feedToAdd" :value="choice.url" v-model="feedNewChoiceSelected">
|
||||
<div class="selectgroup-label">
|
||||
<div>{{ choice.title }}</div>
|
||||
<div :class="{light: choice.title}">{{ choice.url }}</div>
|
||||
<div class="text-truncate">{{ choice.title }}</div>
|
||||
<div class="text-truncate" :class="{light: choice.title}">{{ choice.url }}</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
@@ -299,7 +305,7 @@
|
||||
<template v-slot:button-content>
|
||||
<span class="icon">{% inline "more-vertical.svg" %}</span>
|
||||
</template>
|
||||
<b-dropdown-header>{{folder.title}}</b-dropdown-header>
|
||||
<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"
|
||||
@@ -320,7 +326,7 @@
|
||||
<template v-slot:button-content>
|
||||
<span class="icon">{% inline "more-vertical.svg" %}</span>
|
||||
</template>
|
||||
<b-dropdown-header>{{feed.title}}</b-dropdown-header>
|
||||
<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>
|
||||
|
@@ -100,7 +100,7 @@ Vue.component('relative-time', {
|
||||
'interval': null,
|
||||
}
|
||||
},
|
||||
template: '<time :datetime="val">{{formatted}}</time>',
|
||||
template: '<time :datetime="val">{{ formatted }}</time>',
|
||||
mounted: function() {
|
||||
this.interval = setInterval(function() {
|
||||
this.formatted = dateRepr(this.date)
|
||||
@@ -126,11 +126,11 @@ var vm = new Vue({
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
'filterSelected': null,
|
||||
'filterSelected': undefined,
|
||||
'folders': [],
|
||||
'feeds': [],
|
||||
'feedSelected': null,
|
||||
'feedListWidth': null,
|
||||
'feedSelected': undefined,
|
||||
'feedListWidth': undefined,
|
||||
'feedNewChoice': [],
|
||||
'feedNewChoiceSelected': '',
|
||||
'items': [],
|
||||
@@ -142,8 +142,8 @@ var vm = new Vue({
|
||||
'itemSelectedDetails': {},
|
||||
'itemSelectedReadability': '',
|
||||
'itemSearch': '',
|
||||
'itemSortNewestFirst': null,
|
||||
'itemListWidth': null,
|
||||
'itemSortNewestFirst': undefined,
|
||||
'itemListWidth': undefined,
|
||||
|
||||
'filteredFeedStats': {},
|
||||
'filteredFolderStats': {},
|
||||
@@ -229,13 +229,13 @@ var vm = new Vue({
|
||||
}, 500),
|
||||
},
|
||||
'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))
|
||||
this.itemSelected = null
|
||||
this.computeStats()
|
||||
},
|
||||
'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))
|
||||
this.itemSelected = null
|
||||
if (this.$refs.itemlist) this.$refs.itemlist.scrollTop = 0
|
||||
@@ -259,15 +259,15 @@ var vm = new Vue({
|
||||
this.refreshItems()
|
||||
}, 500),
|
||||
'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))
|
||||
},
|
||||
'feedListWidth': debounce(function(newVal, oldVal) {
|
||||
if (oldVal === null) return
|
||||
if (oldVal === undefined) return // do nothing, initial setup
|
||||
api.settings.update({feed_list_width: newVal})
|
||||
}, 1000),
|
||||
'itemListWidth': debounce(function(newVal, oldVal) {
|
||||
if (oldVal === null) return
|
||||
if (oldVal === undefined) return // do nothing, initial setup
|
||||
api.settings.update({item_list_width: newVal})
|
||||
}, 1000),
|
||||
},
|
||||
@@ -318,9 +318,14 @@ var vm = new Vue({
|
||||
})
|
||||
},
|
||||
refreshItems: function() {
|
||||
if (this.feedSelected === null) {
|
||||
vm.items = []
|
||||
vm.itemsPage = {'cur': 1, 'num': 1}
|
||||
return
|
||||
}
|
||||
var query = this.getItemsQuery()
|
||||
this.loading.items = true
|
||||
api.items.list(query).then(function(data) {
|
||||
return api.items.list(query).then(function(data) {
|
||||
vm.items = data.list
|
||||
vm.itemsPage = data.page
|
||||
vm.loading.items = false
|
||||
@@ -420,10 +425,17 @@ var vm = new Vue({
|
||||
deleteFeed: function(feed) {
|
||||
if (confirm('Are you sure you want to delete ' + feed.title + '?')) {
|
||||
api.feeds.delete(feed.id).then(function() {
|
||||
if (vm.feedSelected === 'feed:'+feed.id) {
|
||||
vm.items = []
|
||||
vm.feedSelected = ''
|
||||
// note: if item list contains delete feed's entries, refresh it first.
|
||||
for (var i = 0; i < vm.items.length; i++) {
|
||||
if (vm.items[i].feed_id == feed.id) {
|
||||
vm.refreshItems().then(function() {
|
||||
vm.refreshStats()
|
||||
vm.refreshFeeds()
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
vm.refreshStats()
|
||||
vm.refreshFeeds()
|
||||
})
|
||||
|
@@ -6,12 +6,6 @@ body {
|
||||
font-size: 15px !important;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
max-width: 1440px;
|
||||
margin: 0 auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* bootstrap customizations */
|
||||
|
||||
.btn-link {
|
||||
@@ -418,6 +412,7 @@ select.form-control:not([multiple]):not([size]) {
|
||||
|
||||
.content pre {
|
||||
color: inherit;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
.content a {
|
||||
@@ -430,10 +425,6 @@ select.form-control:not([multiple]):not([size]) {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.content pre {
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
.content h1 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
@@ -519,3 +510,71 @@ a,
|
||||
opacity: 0;
|
||||
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
37
hacking.md
Normal 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!
|
5
main.go
5
main.go
@@ -11,7 +11,7 @@ import (
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var Version string = "v0.0"
|
||||
var Version string = "0.0"
|
||||
var GitHash string = "unknown"
|
||||
|
||||
func main() {
|
||||
@@ -23,7 +23,7 @@ func main() {
|
||||
flag.Parse()
|
||||
|
||||
if ver {
|
||||
fmt.Printf("%s (%s)\n", Version, GitHash)
|
||||
fmt.Printf("v%s (%s)\n", Version, GitHash)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -48,5 +48,6 @@ func main() {
|
||||
}
|
||||
|
||||
srv := server.New(db, logger, addr)
|
||||
logger.Printf("starting server at http://%s", addr)
|
||||
platform.Start(srv)
|
||||
}
|
||||
|
13
makefile
13
makefile
@@ -1,4 +1,4 @@
|
||||
VERSION=v1.0
|
||||
VERSION=1.1
|
||||
GITHASH=$(shell git rev-parse --short=8 HEAD)
|
||||
|
||||
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
|
||||
|
||||
server/assets_bundle.go: $(ASSETS)
|
||||
server/assets.go: $(ASSETS)
|
||||
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
|
||||
set GOOS=darwin
|
||||
@@ -20,7 +24,7 @@ build_macos: bundle
|
||||
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 _output/macos
|
||||
go run scripts/package_macos.go -outdir _output/macos -version "$(VERSION)"
|
||||
|
||||
build_linux: bundle
|
||||
set GOOS=linux
|
||||
@@ -32,5 +36,6 @@ build_windows: bundle
|
||||
set GOOS=windows
|
||||
set GOARCH=386
|
||||
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
|
||||
|
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
func Start(s *server.Handler) {
|
||||
systrayOnReady := func() {
|
||||
systray.SetIcon(server.Icon)
|
||||
systray.SetIcon(Icon)
|
||||
|
||||
menuOpen := systray.AddMenuItem("Open", "")
|
||||
systray.AddSeparator()
|
||||
|
@@ -1,8 +1,8 @@
|
||||
// +build !windows
|
||||
// +build macos
|
||||
|
||||
// File generated by 2goarray v0.1.0 (http://github.com/cratonica/2goarray)
|
||||
|
||||
package server
|
||||
package platform
|
||||
|
||||
var Icon []byte = []byte{
|
||||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
|
@@ -2,7 +2,7 @@
|
||||
|
||||
// File generated by 2goarray v0.1.0 (http://github.com/cratonica/2goarray)
|
||||
|
||||
package server
|
||||
package platform
|
||||
|
||||
var Icon []byte = []byte{
|
||||
0x00, 0x00, 0x01, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
|
30
readme.md
30
readme.md
@@ -1,4 +1,4 @@
|
||||
# yarr (beta)
|
||||
# yarr
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## build
|
||||
|
||||
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
|
||||
[download](https://github.com/nkanaev/yarr/releases/latest)
|
||||
|
||||
## credits
|
||||
|
||||
[Feather](http://feathericons.com/) for icons.
|
||||
|
||||
## code of conduct
|
||||
|
||||
Be excellent to each other. Party on, dudes!
|
||||
|
@@ -92,5 +92,5 @@ func main() {
|
||||
var buf bytes.Buffer
|
||||
template := template.Must(template.New("code").Parse(code_template))
|
||||
template.Execute(&buf, assets)
|
||||
ioutil.WriteFile("server/assets_bundle.go", buf.Bytes(), 0644)
|
||||
ioutil.WriteFile("server/assets.go", buf.Bytes(), 0644)
|
||||
}
|
||||
|
48
scripts/generate_versioninfo.go
Normal file
48
scripts/generate_versioninfo.go
Normal 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)
|
||||
}
|
@@ -1,13 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
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>
|
||||
<string>nkanaev.yarr</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<string>VERSION</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
@@ -35,11 +37,6 @@ var plist = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>True</string>
|
||||
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.13</string>
|
||||
<key>LSUIElement</key>
|
||||
@@ -59,7 +56,11 @@ func run(cmd ...string) {
|
||||
}
|
||||
|
||||
func main() {
|
||||
outdir := os.Args[1]
|
||||
var version, outdir string
|
||||
flag.StringVar(&version, "version", "0.0", "")
|
||||
flag.StringVar(&outdir, "outdir", "", "")
|
||||
flag.Parse()
|
||||
|
||||
outfile := "yarr"
|
||||
|
||||
binDir := path.Join(outdir, "yarr.app", "Contents/MacOS")
|
||||
@@ -74,7 +75,7 @@ func main() {
|
||||
f, _ := ioutil.ReadFile(path.Join(outdir, outfile))
|
||||
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)
|
||||
|
||||
iconFile := path.Join(outdir, "icon.png")
|
||||
|
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/mmcdole/gofeed"
|
||||
"github.com/nkanaev/yarr/storage"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
@@ -157,13 +158,16 @@ func findFavicon(websiteUrl, feedUrl string) (*[]byte, error) {
|
||||
}
|
||||
|
||||
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)
|
||||
base, err := url.Parse(websiteUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -258,10 +262,16 @@ func listItems(f storage.Feed) ([]storage.Item, error) {
|
||||
}
|
||||
|
||||
func init() {
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
transport.DisableKeepAlives = true
|
||||
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 * 5,
|
||||
Timeout: time.Second * 30,
|
||||
Transport: transport,
|
||||
}
|
||||
defaultClient = &Client{
|
||||
|
@@ -32,7 +32,10 @@ func New(db *storage.Storage, logger *log.Logger, addr string) *Handler {
|
||||
func (h *Handler) Start() {
|
||||
h.startJobs()
|
||||
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) {
|
||||
@@ -75,13 +78,12 @@ func (h *Handler) startJobs() {
|
||||
h.db.CreateItems(items)
|
||||
syncSearch()
|
||||
if !feed.HasIcon {
|
||||
h.log.Println("searching favicon for:", feed.Link, feed.FeedLink)
|
||||
icon, err := findFavicon(feed.Link, feed.FeedLink)
|
||||
if icon != nil {
|
||||
h.db.UpdateFeedIcon(feed.Id, icon)
|
||||
}
|
||||
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:
|
||||
|
Reference in New Issue
Block a user