Files
magpie/internal/update/notes.go
T
yetone f22218a092 gui: after an update the window shows what changed since the version last run (a Discord user: 点更新后能弹个窗显示更新了啥吗, to see whether their issue was fixed) — magpie updated itself silently, and its release notes were only on GitHub. The version last run is now kept in magpie's own folder (last-version); started on a newer release, the window or magpie web's page asks usemagpie.ai's new /api/notes (beside /api/latest; site/worker.js lists the releases' notes from GitHub, cached as the feed is) for every release since, newest first, without their Install section, and shows them once in a What's new dialog. A fresh install, the same version, a downgrade or a build from source shows nothing; a magpie from before the version was kept, already in use, shows its own version's notes; offline nothing is shown and nothing waits on it; until the site is deployed, the update feed's newest notes stand in when they are the current version's. The tray's panel shows no dialog. The notes are drawn as text, never as HTML: headings, bullets, bold, code, http(s) links, and #NNN links to the issue, opened in the browser as the app's other links are. Settings has a What's new row under Version that opens them again, with a waiting update's notes first, marked Not installed yet; /api/update's notes leave out the Install section too. TestShowNotes, TestStripInstall, TestBetween, TestNotesBetween, TestWhatsNewAfterUpgrade, TestWhatsNewFromBefore, TestUpdateNotesWithoutInstall and whats-new.test.cjs (Chromium, WebKit, en, zh) fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; click-scroll, old-webkit, update-check, update-hide pass.
2026-10-02 03:57:55 +08:00

139 lines
4.0 KiB
Go

package update
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"regexp"
"sort"
"strings"
)
// Note is one release's notes, as the app shows them after an update.
type Note struct {
Version string `json:"version"` // no v
Notes string `json:"notes"` // markdown, its Install section taken out
URL string `json:"url,omitempty"`
}
// NotesFeed is where the notes of past releases are listed: beside the
// update feed, on magpie's site. MAGPIE_NOTES_FEED points it elsewhere.
func NotesFeed() string {
if f := os.Getenv("MAGPIE_NOTES_FEED"); f != "" {
return f
}
return Site + "/api/notes"
}
// NotesBetween asks the site for the notes of every release after after
// (none when "") up to and including upto, newest first.
func NotesBetween(ctx context.Context, after, upto string) ([]Note, error) {
q := url.Values{"upto": {upto}}
if after != "" {
q.Set("after", after)
}
req, err := http.NewRequestWithContext(ctx, "GET", NotesFeed()+"?"+q.Encode(), nil)
if err != nil {
return nil, err
}
res, err := client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != 200 {
return nil, fmt.Errorf("release notes: %s", res.Status)
}
var out struct {
Releases []Note `json:"releases"`
}
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
return nil, fmt.Errorf("release notes: %w", err)
}
return Between(out.Releases, after, upto), nil
}
// MaxNotes is how many releases' notes are shown at most: magpie releases
// often, and a long-unopened one would have pages of them.
const MaxNotes = 40
// Between keeps the notes of the releases after after up to and including
// upto, newest first, at most MaxNotes; with after "" only upto's. Each
// one's Install section is taken out, and one left empty is dropped.
func Between(notes []Note, after, upto string) []Note {
var out []Note
seen := map[string]bool{}
for _, n := range notes {
v := strings.TrimPrefix(strings.TrimSpace(n.Version), "v")
if parse(v) == nil || seen[v] || Newer(v, upto) {
continue
}
if after == "" {
if Newer(upto, v) {
continue
}
} else if !Newer(v, after) {
continue
}
n.Version, n.Notes = v, StripInstall(n.Notes)
if strings.TrimSpace(n.Notes) == "" {
continue
}
seen[v] = true
out = append(out, n)
}
sort.SliceStable(out, func(i, j int) bool { return Newer(out[i].Version, out[j].Version) })
if len(out) > MaxNotes {
out = out[:MaxNotes]
}
return out
}
var heading = regexp.MustCompile(`^(#{1,6})\s+(.*?)\s*#*\s*$`)
// StripInstall takes a release's "Install" section out of its notes: the
// download links, which mean nothing to a magpie that just updated itself.
// The section runs to the next heading of its level or above.
func StripInstall(md string) string {
var out []string
skip := 0 // the level of the Install heading being skipped, 0 when not
for _, line := range strings.Split(strings.ReplaceAll(md, "\r\n", "\n"), "\n") {
if m := heading.FindStringSubmatch(line); m != nil {
level := len(m[1])
if skip > 0 && level <= skip {
skip = 0
}
if strings.EqualFold(m[2], "Install") {
skip = level
}
}
if skip == 0 {
out = append(out, line)
}
}
return strings.TrimSpace(strings.Join(out, "\n"))
}
// ShowNotes decides, as magpie starts on current having last run as last
// ("" when it never said), whether to show what changed since, and from
// which version on (exclusive). Only an upgrade between releases shows:
// not a fresh install, the same version again, a downgrade, or a build
// from source. A magpie from before versions were kept has no last; when
// it was already in use (existed) its own version's notes are shown, from
// "", which Between takes as upto's alone.
func ShowNotes(last, current string, existed bool) (after string, show bool) {
if !Released(current) {
return "", false
}
if last == "" {
return "", existed
}
if !Newer(current, last) {
return "", false
}
return strings.TrimPrefix(strings.TrimSpace(last), "v"), true
}