mirror of
https://github.com/nkanaev/yarr.git
synced 2025-09-13 09:55:36 +00:00
Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
e3109a4384 | ||
|
eee8002d69 | ||
|
92f11f7513 | ||
|
5428e6be3a | ||
|
1ad693f931 | ||
|
c2d88a7e3f | ||
|
3b29d737eb | ||
|
fe178b8fc6 | ||
|
cca742a1c2 | ||
|
c7eddff118 | ||
|
cf30ed249f | ||
|
26b87dee98 | ||
|
77c7f938f1 | ||
|
f98de9a0a5 | ||
|
6fa2b67024 | ||
|
355e5feb62 | ||
|
a7dd707062 |
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -131,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
|
||||
@@ -140,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
2
.gitignore
vendored
@@ -1,5 +1,3 @@
|
||||
/server/assets.go
|
||||
/gofeed
|
||||
/_output
|
||||
/yarr
|
||||
*.db
|
||||
|
@@ -1,5 +1,12 @@
|
||||
# upcoming
|
||||
|
||||
- (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)
|
||||
|
@@ -20,3 +20,8 @@ The licenses are included, and the authorship comments are left intact.
|
||||
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
|
||||
|
1
doc/todo.txt
Normal file
1
doc/todo.txt
Normal file
@@ -0,0 +1 @@
|
||||
- feedlist keyboard navigation is flaky in "unread" section
|
BIN
etc/promo.png
BIN
etc/promo.png
Binary file not shown.
Before Width: | Height: | Size: 727 KiB After Width: | Height: | Size: 430 KiB |
2
makefile
2
makefile
@@ -1,4 +1,4 @@
|
||||
VERSION=2.1
|
||||
VERSION=2.2
|
||||
GITHASH=$(shell git rev-parse --short=8 HEAD)
|
||||
|
||||
CGO_ENABLED=1
|
||||
|
@@ -57,6 +57,18 @@
|
||||
|
||||
<div class="dropdown-divider"></div>
|
||||
|
||||
<header class="dropdown-header">Theme</header>
|
||||
<div class="row text-center m-0">
|
||||
<button class="btn btn-link col-4 px-0 rounded-0"
|
||||
:class="'theme-'+t"
|
||||
@click.stop="theme.name = t"
|
||||
v-for="t in ['light', 'sepia', 'night']">
|
||||
<span class="icon" v-if="theme.name == t">{% inline "check.svg" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="dropdown-divider"></div>
|
||||
|
||||
<header class="dropdown-header">Auto Refresh</header>
|
||||
<div class="row text-center m-0">
|
||||
<button class="dropdown-item col-4 px-0" :class="{active: !refreshRate}" @click.stop="refreshRate = 0">0</button>
|
||||
@@ -117,6 +129,7 @@
|
||||
<div v-for="folder in foldersWithFeeds">
|
||||
<label class="selectgroup mt-1"
|
||||
:class="{'d-none': filterSelected
|
||||
&& !(current.folder.id == folder.id || current.feed.folder_id == folder.id)
|
||||
&& !filteredFolderStats[folder.id]
|
||||
&& (!itemSelectedDetails || feedsById[itemSelectedDetails.feed_id].folder_id != folder.id)}">
|
||||
<input type="radio" name="feed" :value="'folder:'+folder.id" v-model="feedSelected">
|
||||
@@ -133,6 +146,7 @@
|
||||
<div v-show="!folder.id || folder.is_expanded" class="mt-1" :class="{'pl-3': folder.id}">
|
||||
<label class="selectgroup"
|
||||
:class="{'d-none': filterSelected
|
||||
&& !(current.feed.id == feed.id)
|
||||
&& !filteredFeedStats[feed.id]
|
||||
&& (!itemSelectedDetails || itemSelectedDetails.feed_id != feed.id)}"
|
||||
v-for="feed in folder.feeds">
|
||||
@@ -177,11 +191,16 @@
|
||||
title="Mark All Read">
|
||||
<span class="icon">{% inline "check.svg" %}</span>
|
||||
</button>
|
||||
|
||||
|
||||
<button class="btn btn-link toolbar-item px-2 ml-2" v-if="!current.type" disabled>
|
||||
<span class="icon">{% inline "more-horizontal.svg" %}</span>
|
||||
</button>
|
||||
<dropdown class="settings-dropdown"
|
||||
toggle-class="btn btn-link toolbar-item px-2 ml-2"
|
||||
drop="right"
|
||||
title="Feed Settings"
|
||||
v-if="!filterSelected && current.type == 'feed'">
|
||||
v-if="current.type == 'feed'">
|
||||
<template v-slot:button>
|
||||
<span class="icon">{% inline "more-horizontal.svg" %}</span>
|
||||
</template>
|
||||
@@ -226,7 +245,7 @@
|
||||
toggle-class="btn btn-link toolbar-item px-2 ml-2"
|
||||
title="Folder Settings"
|
||||
drop="right"
|
||||
v-if="!filterSelected && current.type == 'folder'">
|
||||
v-if="current.type == 'folder'">
|
||||
<template v-slot:button>
|
||||
<span class="icon">{% inline "more-horizontal.svg" %}</span>
|
||||
</template>
|
||||
@@ -285,14 +304,6 @@
|
||||
<template v-slot:button>
|
||||
<span class="icon">{% inline "sliders.svg" %}</span>
|
||||
</template>
|
||||
<div class="row text-center m-0">
|
||||
<button class="btn btn-link col-4 px-0 rounded-0"
|
||||
:class="'theme-'+t"
|
||||
@click.stop="theme.name = t"
|
||||
v-for="t in ['light', 'sepia', 'night']">
|
||||
<span class="icon" v-if="theme.name == t">{% inline "check.svg" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button class="dropdown-item" :class="{active: !theme.font}" @click.stop="theme.font = ''">sans-serif</button>
|
||||
<button class="dropdown-item font-serif" :class="{active: theme.font == 'serif'}" @click.stop="theme.font = 'serif'">serif</button>
|
||||
@@ -350,7 +361,7 @@
|
||||
</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>
|
||||
<option :value="folder.id" v-for="folder in folders" :selected="folder.id === current.feed.folder_id || folder.id === current.folder.id">{{ folder.title }}</option>
|
||||
</select>
|
||||
<div class="mt-4" v-if="feedNewChoice.length">
|
||||
<p class="mb-2">
|
||||
|
@@ -436,6 +436,7 @@ var vm = new Vue({
|
||||
vm.items = []
|
||||
vm.itemsPage = {'cur': 1, 'num': 1}
|
||||
vm.itemSelected = null
|
||||
vm.itemsHasMore = false
|
||||
vm.refreshStats()
|
||||
})
|
||||
},
|
||||
|
@@ -29,6 +29,8 @@ func opt(envVar, defaultValue string) string {
|
||||
}
|
||||
|
||||
func main() {
|
||||
platform.FixConsoleIfNeeded()
|
||||
|
||||
var addr, db, authfile, certfile, keyfile, basepath, logfile string
|
||||
var ver, open bool
|
||||
|
||||
|
@@ -9,6 +9,8 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/nkanaev/yarr/src/content/htmlutil"
|
||||
)
|
||||
|
||||
var UnknownFormat = errors.New("unknown feed format")
|
||||
@@ -80,7 +82,7 @@ func (feed *Feed) cleanup() {
|
||||
for i, item := range feed.Items {
|
||||
feed.Items[i].GUID = strings.TrimSpace(item.GUID)
|
||||
feed.Items[i].URL = strings.TrimSpace(item.URL)
|
||||
feed.Items[i].Title = strings.TrimSpace(item.Title)
|
||||
feed.Items[i].Title = strings.TrimSpace(htmlutil.ExtractText(item.Title))
|
||||
feed.Items[i].Content = strings.TrimSpace(item.Content)
|
||||
|
||||
if item.ImageURL != "" && strings.Contains(item.Content, item.ImageURL) {
|
||||
|
@@ -180,3 +180,26 @@ func TestRSSPodcastDuplicated(t *testing.T) {
|
||||
t.Fatal("item.audio_url must be unset if present in the content")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRSSTitleHTMLTags(t *testing.T) {
|
||||
feed, _ := Parse(strings.NewReader(`
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
|
||||
<channel>
|
||||
<item>
|
||||
<title><p>title in p</p></title>
|
||||
</item>
|
||||
<item>
|
||||
<title>very <strong>strong</strong> title</title>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
`))
|
||||
have := []string{feed.Items[0].Title, feed.Items[1].Title}
|
||||
want := []string{"title in p", "very strong title"}
|
||||
for i := 0; i < len(want); i++ {
|
||||
if want[i] != have[i] {
|
||||
t.Errorf("title doesn't match\nwant: %#v\nhave: %#v\n", want[i], have[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
14
src/platform/fixconsole_default.go
Normal file
14
src/platform/fixconsole_default.go
Normal file
@@ -0,0 +1,14 @@
|
||||
// +build !windows
|
||||
|
||||
package platform
|
||||
|
||||
// On non-windows platforms, we don't need to do anything. The console
|
||||
// starts off attached already, if it exists.
|
||||
|
||||
func AttachConsole() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func FixConsoleIfNeeded() error {
|
||||
return nil
|
||||
}
|
131
src/platform/fixconsole_windows.go
Normal file
131
src/platform/fixconsole_windows.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"golang.org/x/sys/windows"
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func AttachConsole() error {
|
||||
const ATTACH_PARENT_PROCESS = ^uintptr(0)
|
||||
proc := syscall.MustLoadDLL("kernel32.dll").MustFindProc("AttachConsole")
|
||||
r1, _, err := proc.Call(ATTACH_PARENT_PROCESS)
|
||||
if r1 == 0 {
|
||||
errno, ok := err.(syscall.Errno)
|
||||
if ok && errno == windows.ERROR_INVALID_HANDLE {
|
||||
// console handle doesn't exist; not a real
|
||||
// error, but the console handle will be
|
||||
// invalid.
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var oldStdin, oldStdout, oldStderr *os.File
|
||||
|
||||
// Windows console output is a mess.
|
||||
//
|
||||
// If you compile as "-H windows", then if you launch your program without
|
||||
// a console, Windows forcibly creates one to use as your stdin/stdout, which
|
||||
// is silly for a GUI app, so we can't do that.
|
||||
//
|
||||
// If you compile as "-H windowsgui", then it doesn't create a console for
|
||||
// your app... but also doesn't provide a working stdin/stdout/stderr even if
|
||||
// you *did* launch from the console. However, you can use AttachConsole()
|
||||
// to get a handle to your parent process's console, if any, and then
|
||||
// os.NewFile() to turn that handle into a fd usable as stdout/stderr.
|
||||
//
|
||||
// However, then you have the problem that if you redirect stdout or stderr
|
||||
// from the shell, you end up ignoring the redirection by forcing it to the
|
||||
// console.
|
||||
//
|
||||
// To fix *that*, we have to detect whether there was a pre-existing stdout
|
||||
// or not. We can check GetStdHandle(), which returns 0 for "should be
|
||||
// console" and nonzero for "already pointing at a file."
|
||||
//
|
||||
// Be careful though! As soon as you run AttachConsole(), it resets *all*
|
||||
// the GetStdHandle() handles to point them at the console instead, thus
|
||||
// throwing away the original file redirects. So we have to GetStdHandle()
|
||||
// *before* AttachConsole().
|
||||
//
|
||||
// For some reason, powershell redirections provide a valid file handle, but
|
||||
// writing to that handle doesn't write to the file. I haven't found a way
|
||||
// to work around that. (Windows 10.0.17763.379)
|
||||
//
|
||||
// Net result is as follows.
|
||||
// Before:
|
||||
// SHELL NON-REDIRECTED REDIRECTED
|
||||
// explorer.exe no console n/a
|
||||
// cmd.exe broken works
|
||||
// powershell broken broken
|
||||
// WSL bash broken works
|
||||
// After
|
||||
// SHELL NON-REDIRECTED REDIRECTED
|
||||
// explorer.exe no console n/a
|
||||
// cmd.exe works works
|
||||
// powershell works broken
|
||||
// WSL bash works works
|
||||
//
|
||||
// We don't seem to make anything worse, at least.
|
||||
func FixConsoleIfNeeded() error {
|
||||
// Retain the original console objects, to prevent Go from automatically
|
||||
// closing their file descriptors when they get garbage collected.
|
||||
// You never want to close file descriptors 0, 1, and 2.
|
||||
oldStdin, oldStdout, oldStderr = os.Stdin, os.Stdout, os.Stderr
|
||||
|
||||
stdin, _ := syscall.GetStdHandle(syscall.STD_INPUT_HANDLE)
|
||||
stdout, _ := syscall.GetStdHandle(syscall.STD_OUTPUT_HANDLE)
|
||||
stderr, _ := syscall.GetStdHandle(syscall.STD_ERROR_HANDLE)
|
||||
|
||||
var invalid syscall.Handle
|
||||
con := invalid
|
||||
|
||||
if stdin == invalid || stdout == invalid || stderr == invalid {
|
||||
err := AttachConsole()
|
||||
if err != nil {
|
||||
return fmt.Errorf("attachconsole: %v", err)
|
||||
}
|
||||
|
||||
if stdin == invalid {
|
||||
stdin, _ = syscall.GetStdHandle(syscall.STD_INPUT_HANDLE)
|
||||
}
|
||||
if stdout == invalid {
|
||||
stdout, _ = syscall.GetStdHandle(syscall.STD_OUTPUT_HANDLE)
|
||||
con = stdout
|
||||
}
|
||||
if stderr == invalid {
|
||||
stderr, _ = syscall.GetStdHandle(syscall.STD_ERROR_HANDLE)
|
||||
con = stderr
|
||||
}
|
||||
}
|
||||
|
||||
if con != invalid {
|
||||
// Make sure the console is configured to convert
|
||||
// \n to \r\n, like Go programs expect.
|
||||
h := windows.Handle(con)
|
||||
var st uint32
|
||||
err := windows.GetConsoleMode(h, &st)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetConsoleMode: %v", err)
|
||||
}
|
||||
err = windows.SetConsoleMode(h, st&^windows.DISABLE_NEWLINE_AUTO_RETURN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SetConsoleMode: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if stdin != invalid {
|
||||
os.Stdin = os.NewFile(uintptr(stdin), "stdin")
|
||||
}
|
||||
if stdout != invalid {
|
||||
os.Stdout = os.NewFile(uintptr(stdout), "stdout")
|
||||
}
|
||||
if stderr != invalid {
|
||||
os.Stderr = os.NewFile(uintptr(stderr), "stderr")
|
||||
}
|
||||
return nil
|
||||
}
|
Reference in New Issue
Block a user