1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# frozen_string_literal: true
require "phlex"
module RankKing
module Views
module Sources
# Where a youtube.com cookie is pasted, on the new-list form and in
# the sync dialog. rank-king keeps it only for the one fetch.
class YouTubeCookie < Phlex::HTML
def view_template
label(class: "field") do
plain "YouTube cookie"
textarea(name: "cookie", rows: 3, required: true, autocomplete: "off", spellcheck: "false")
small { "The Cookie request header from a signed-in youtube.com tab, copied from your browser's developer tools. It is used for this fetch and not kept." }
end
end
end
# The YouTube playlist source's fields on the new-list form: a cookie,
# then a choice of the playlists it opens, which app.js fetches into
# [data-playlists] once the cookie is pasted.
class YouTubeFields < Phlex::HTML
def initialize(csrf:)
@csrf = csrf
end
def view_template
path = RankKing::Sources::YouTube::PLAYLISTS_PATH
render YouTubeCookie.new
div(data_playlists: path, data_csrf: @csrf.call(path)) do
render YouTubePlaylists.new(playlists: nil)
end
end
end
# The choice of playlists, or why there is none yet. playlists is nil
# until a cookie is pasted.
class YouTubePlaylists < Phlex::HTML
def initialize(playlists:, error: nil)
@playlists = playlists
@error = error
end
def view_template
if @error
p { "YouTube would not list your playlists: #{@error}" }
elsif @playlists.nil?
p { "Paste a cookie to choose one of your playlists." }
elsif @playlists.empty?
p { "Your YouTube account has no playlists yet." }
else
label(class: "field") do
plain "Playlist"
select(name: "playlist", required: true) do
@playlists.each do |id, title|
option(value: id) { title }
end
end
small { "Watch Later and private playlists are listed too." }
end
end
end
end
# A video as its thumbnail, title, and channel, so two in a matchup can
# be told apart. A video synced without a thumbnail or channel shows
# whatever it has. The page lays it out: a row on the list, stacked
# in a matchup.
class YouTubeItem < Phlex::HTML
def initialize(title:, channel:, thumbnail:)
@title = title
@channel = channel
@thumbnail = thumbnail
end
def view_template
span(data_video: true) do
img(src: @thumbnail, alt: "", loading: "lazy") if @thumbnail
span do
span { @title }
small { @channel } if @channel
end
end
end
end
# The YouTube playlist source's sync dialog: a fresh cookie to fetch
# with, and what syncing does, shown working.
class YouTube < Phlex::HTML
def initialize(list:, csrf:)
@list = list
@csrf = csrf
end
def view_template
action = "/lists/#{@list.id}/sync"
header { "Sync from YouTube" }
form(id: "sync-form", method: "post", action:, data_busy: "Syncing…") do
input(type: "hidden", name: "_csrf", value: @csrf.call(action))
p { "Fetches the playlist again. New videos are added, and videos that left it are hidden." }
render YouTubeCookie.new
if (synced_at = @list.synced_at)
small { "Last synced #{synced_at.strftime("%b %-d, %Y at %-I:%M %p")}." }
end
end
footer do
button(type: "button", commandfor: "sync-dialog", command: "close") { "Cancel" }
button(type: "submit", form: "sync-form", data_variant: "primary") { "Sync" }
end
end
end
end
end
end