Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/curd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ func main() {
flag.StringVar(&userCurdConfig.DiscordClientId, "discord-client-id", userCurdConfig.DiscordClientId, "Discord client ID for Rich Presence")
flag.BoolVar(&userCurdConfig.VimKeys, "vim-keys", userCurdConfig.VimKeys, "Enable vim motions in selection menus (j/k/h/l, / search) (true/false)")
flag.BoolVar(&userCurdConfig.CheckUpdates, "check-updates", userCurdConfig.CheckUpdates, "Check for curd updates in the background when idle (true/false)")
flag.BoolVar(&userCurdConfig.ShowNewEpisodes, "show-new-episodes", userCurdConfig.ShowNewEpisodes, "Show new episode indicators in currently watching list (true/false)")
continueLast := flag.Bool("c", false, "Continue last episode")
addNewAnime := flag.Bool("new", false, "Add new anime")
rofiSelection := flag.Bool("rofi", false, "Open selection in rofi")
Expand Down
15 changes: 15 additions & 0 deletions internal/anilist.go
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,10 @@ func GetUserData(token string, userID int) (map[string]interface{}, error) {
native
}
status
nextAiringEpisode {
episode
timeUntilAiring
}
}
status
score
Expand Down Expand Up @@ -597,6 +601,10 @@ func GetUserDataPreview(token string, userID int) (map[string]interface{}, error
native
}
status
nextAiringEpisode {
episode
timeUntilAiring
}
}
status
score
Expand Down Expand Up @@ -1171,6 +1179,13 @@ func ParseAnimeList(input map[string]interface{}) AnimeList {
animeEntry.CoverImage = safeString(coverImage["large"])
}

if nextEp, ok := media["nextAiringEpisode"].(map[string]interface{}); ok && nextEp != nil {
animeEntry.Media.NextAiringEpisode = &NextAiringEpisodeInfo{
Episode: toInt(nextEp["episode"]),
TimeUntilAiring: toInt(nextEp["timeUntilAiring"]),
}
}

// Defense in depth: never insert the same media twice even if AniList
// returns the entry under multiple non-custom lists.
if animeEntry.Media.ID != 0 {
Expand Down
22 changes: 18 additions & 4 deletions internal/anilist_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,16 @@ func buildCategorySelectionOptions(list AnimeList, category string) []SelectionO
for _, entry := range getEntriesByCategory(list, category) {
title := mediaDisplayTitle(entry.Media, userCurdConfig)

hasNew := false
if userCurdConfig.ShowNewEpisodes && entry.Media.NextAiringEpisode != nil {
nextWatched := nextEpisodeFromProgress(entry.Progress)
hasNew = entry.Media.NextAiringEpisode.Episode > nextWatched
}

options = append(options, SelectionOption{
Key: strconv.Itoa(entry.Media.ID),
Label: title,
Key: strconv.Itoa(entry.Media.ID),
Label: title,
HasNewEpisodes: hasNew,
})
}

Expand All @@ -285,9 +292,16 @@ func buildCategoryPreviewOptions(list AnimeList, category string) map[string]Rof
for _, entry := range getEntriesByCategory(list, category) {
title := mediaDisplayTitle(entry.Media, userCurdConfig)

hasNew := false
if userCurdConfig.ShowNewEpisodes && entry.Media.NextAiringEpisode != nil {
nextWatched := nextEpisodeFromProgress(entry.Progress)
hasNew = entry.Media.NextAiringEpisode.Episode > nextWatched
}

options[strconv.Itoa(entry.Media.ID)] = RofiSelectPreview{
Title: title,
CoverImage: entry.CoverImage,
Title: title,
CoverImage: entry.CoverImage,
HasNewEpisodes: hasNew,
}
}

Expand Down
2 changes: 2 additions & 0 deletions internal/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ type CurdConfig struct {
MyAnimeListClientSecret string `config:"MyAnimeListClientSecret"`
MyAnimeListImported bool `config:"MyAnimeListImported"`
MyAnimeListImportDismissed bool `config:"MyAnimeListImportDismissed"`
ShowNewEpisodes bool `config:"ShowNewEpisodes"`
}

const DefaultMpvPlaybackStartTimeout = 20
Expand Down Expand Up @@ -134,6 +135,7 @@ func defaultConfigMap() map[string]string {
"MyAnimeListClientSecret": "",
"MyAnimeListImported": "false",
"MyAnimeListImportDismissed": "false",
"ShowNewEpisodes": "true",
}
}

Expand Down
64 changes: 50 additions & 14 deletions internal/selection_menu.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ var (

quitHintStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#FFD700")) // Gold

newEpisodeItemStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#4CAF50")) // Green

rofiNewEpisodeColor = "#4CAF50"
)

// Init initializes the model
Expand Down Expand Up @@ -318,10 +323,18 @@ func (m Model) View() string {

// Render the options within the visible range
for i := start; i < end; i++ {
label := m.filteredKeys[i].Label

if i == m.selected {
b.WriteString(selectedItemStyle.Render(m.filteredKeys[i].Label) + "\n")
if m.filteredKeys[i].HasNewEpisodes {
b.WriteString(newEpisodeItemStyle.Render(" [NEW]") + selectedItemStyle.Render(label) + "\n")
} else {
b.WriteString(selectedItemStyle.Render(label) + "\n")
}
} else if m.filteredKeys[i].HasNewEpisodes {
b.WriteString(newEpisodeItemStyle.Render(" [NEW]") + regularItemStyle.Render(label) + "\n")
} else {
b.WriteString(regularItemStyle.Render(m.filteredKeys[i].Label) + "\n")
b.WriteString(regularItemStyle.Render(label) + "\n")
}
}
}
Expand All @@ -339,11 +352,19 @@ func (m Model) visibleItemsCount() int {
return count
}

func displayLabel(opt SelectionOption) string {
if opt.HasNewEpisodes {
return "[NEW]" + opt.Label
}
return opt.Label
}

// filterOptions filters and sorts options based on the search term
func (m *Model) filterOptions() {
m.filteredKeys = nil
for _, opt := range m.allOptions {
if strings.Contains(strings.ToLower(opt.Label), strings.ToLower(m.filter)) {
// Small function to also consider new episode from list
if strings.Contains(strings.ToLower(displayLabel(opt)), strings.ToLower(m.filter)) {
m.filteredKeys = append(m.filteredKeys, opt)
}
}
Expand Down Expand Up @@ -498,8 +519,9 @@ func previewOptionsToSortedSelection(options map[string]RofiSelectPreview) []Sel
selectionOptions := make([]SelectionOption, 0, len(options))
for id, opt := range options {
selectionOptions = append(selectionOptions, SelectionOption{
Label: opt.Title,
Key: id,
Label: opt.Title,
Key: id,
HasNewEpisodes: opt.HasNewEpisodes,
})
}

Expand Down Expand Up @@ -531,18 +553,22 @@ func DynamicSelectPreviewWithRefresh(options map[string]RofiSelectPreview, addne
Log(fmt.Sprintf("Error caching image: %v", err))
continue
}
rofiInput.WriteString(fmt.Sprintf("%s\x00icon\x1f%s\n", opt.Label, cachePath))
label := opt.Label
if opt.HasNewEpisodes {
label = fmt.Sprintf("<span foreground=\"%s\">[NEW]</span> %s ", rofiNewEpisodeColor, opt.Label)
}
rofiInput.WriteString(fmt.Sprintf("%s\x00icon\x1f%s\n", label, cachePath))
}

if addnewoption {

rofiInput.WriteString("Add new anime\n")
}
rofiInput.WriteString("Back\n")
rofiInput.WriteString("Quit\n")

configPath := filepath.Join(GetStoragePath(), "selectanimepreview.rasi")
cmd := exec.Command("rofi", "-dmenu", "-theme", configPath, "-show-icons", "-p", "Select Anime", "-i", "-no-custom")
// NOTE: Need `-markup-rows` to enable pango
cmd := exec.Command("rofi", "-dmenu", "-theme", configPath, "-show-icons", "-markup-rows", "-p", "Select Anime", "-i", "-no-custom")
cmd.Stdin = strings.NewReader(rofiInput.String())
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
Expand Down Expand Up @@ -613,6 +639,9 @@ func preDownloadImages(options map[string]RofiSelectPreview, count int) {

func parsePreviewSelection(rawSelection string, selectionOptions []SelectionOption) (SelectionOption, error) {
selected := strings.TrimSpace(rawSelection)
selected = strings.TrimSpace(pangoStrip.ReplaceAllString(selected, ""))
selected = strings.TrimPrefix(selected, "[NEW] ")
selected = strings.TrimSpace(selected)

switch selected {
case "":
Expand Down Expand Up @@ -641,7 +670,7 @@ func downloadToCache(imageURL string) (string, error) {
}

cacheDir := os.ExpandEnv("${HOME}/.cache/curd/images")
if err := os.MkdirAll(cacheDir, 0755); err != nil {
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
return "", fmt.Errorf("failed to create cache directory: %w", err)
}

Expand Down Expand Up @@ -725,8 +754,7 @@ func rofiSelectInternal(options []SelectionOption, isHomeMenu bool, refreshConfi
for {
optionsString := buildRofiOptionsString(currentOptions, isHomeMenu)
configPath := filepath.Join(GetStoragePath(), "selectanime.rasi")
// -markup: enable Pango in -mesg (and prompts where supported)
args := []string{"-dmenu", "-theme", configPath, "-i", "-markup", "-p", prompt}
args := []string{"-dmenu", "-theme", configPath, "-i", "-markup", "-markup-rows", "-p", prompt}
if msg := strings.TrimSpace(message); msg != "" {
args = append(args, "-mesg", msg)
}
Expand Down Expand Up @@ -894,7 +922,11 @@ func dynamicSelectInternal(options []SelectionOption, refreshConfig *SelectionRe
func buildRofiOptionsString(options []SelectionOption, isHomeMenu bool) string {
optionsList := make([]string, 0, len(options)+2)
for _, opt := range options {
optionsList = append(optionsList, opt.Label)
if opt.HasNewEpisodes {
optionsList = append(optionsList, fmt.Sprintf("<span foreground=\"%s\">[NEW]</span> %s", rofiNewEpisodeColor, opt.Label))
} else {
optionsList = append(optionsList, opt.Label)
}
}

if !isHomeMenu {
Expand All @@ -917,8 +949,12 @@ func parseRofiSelection(err error, rawSelection string, options []SelectionOptio
}

selected := strings.TrimSpace(rawSelection)
// Strip accidental Pango/markup noise if a theme echoes it.
selected = strings.TrimSpace(ansiStrip.ReplaceAllString(selected, ""))
// strip accidental pango noise if a theme echoes it.
selected = strings.TrimSpace(pangoStrip.ReplaceAllString(
ansiStrip.ReplaceAllString(selected, ""), "",
))
selected = strings.TrimPrefix(selected, "[NEW] ")
selected = strings.TrimSpace(selected)
switch {
case selected == "":
if isHomeMenu {
Expand Down
36 changes: 22 additions & 14 deletions internal/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ type NextEpisode struct {
Mode string
}

type NextAiringEpisodeInfo struct {
Episode int
TimeUntilAiring int
}

// StreamPlaybackHint carries MPV playback metadata for a resolved stream URL.
type StreamPlaybackHint struct {
Referrer string
Expand Down Expand Up @@ -127,13 +132,14 @@ type ResponseData struct {
}

type Media struct {
Duration int `json:"duration"`
Episodes int `json:"episodes"`
Format string `json:"format"`
ID int `json:"id"`
MalID int `json:"mal_id"`
Title AnimeTitle `json:"title"`
Status string `json:"status"`
Duration int `json:"duration"`
Episodes int `json:"episodes"`
Format string `json:"format"`
ID int `json:"id"`
MalID int `json:"mal_id"`
Title AnimeTitle `json:"title"`
Status string `json:"status"`
NextAiringEpisode *NextAiringEpisodeInfo `json:"nextAiringEpisode,omitempty"`
}

type Entry struct {
Expand All @@ -159,8 +165,9 @@ type AnimeList struct {
}

type RofiSelectPreview struct {
Title string `json:"title"`
CoverImage string `json:"coverImage"`
Title string `json:"title"`
CoverImage string `json:"coverImage"`
HasNewEpisodes bool `json:"-"`
}

type SelectionOptionImage struct {
Expand All @@ -171,9 +178,10 @@ type SelectionOptionImage struct {

// SelectionOption holds the label and the internal key
type SelectionOption struct {
Title string
Label string
Key string
Thumbnail string
ExtraData interface{}
Title string
Label string
Key string
Thumbnail string
ExtraData interface{}
HasNewEpisodes bool
}
1 change: 1 addition & 0 deletions internal/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ var (
mdLinkRe = regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`)
mdURLRe = regexp.MustCompile(`https?://[^\s<>\]]+`)
ansiStrip = regexp.MustCompile(`\x1b\[[0-9;]*m`)
pangoStrip = regexp.MustCompile(`<[^>]*>`)
)

// markdownToPango turns common GitHub release markdown into Rofi-friendly Pango.
Expand Down
2 changes: 1 addition & 1 deletion rofi/selectanimepreview.rasi
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
text-color: #BBBBBB; /* Default text color (light gray) */
text-color-selected: #FFFFFF; /* Text color when selected (white) */
primary: rgba(53, 132, 228, 0.75); /* Blusish primary color */
important: rgba(53, 132, 228, 0.75); /* Bluish primary color */
important: rgba(53, 132, 228, 0.75); /* Bluish primary color */
}

configuration {
Expand Down
Loading