mirror of
https://gitea.invidious.io/iv-org/invidious-copy-2022-04-11.git
synced 2024-08-15 00:43:26 +00:00
Merge 3cc70db82b
into fabbecf4c2
This commit is contained in:
commit
80dfd7230b
5 changed files with 313 additions and 82 deletions
62
assets/css/user.css
Normal file
62
assets/css/user.css
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* User pages CSS
|
||||
*
|
||||
* Used on all authenticated pages
|
||||
* (subscriptions, account management, etc...)
|
||||
*
|
||||
* Part of invidious
|
||||
* Copyright iv-org
|
||||
* Licensed under AGPLv3
|
||||
*/
|
||||
|
||||
/*
|
||||
* User menu
|
||||
*/
|
||||
|
||||
.user-menu, .user-tab {
|
||||
margin: 10px 10px 20px 15px;
|
||||
padding: 0;
|
||||
background-color: #262626;
|
||||
}
|
||||
|
||||
|
||||
.user-menu ul {
|
||||
padding: 0; margin: 0;
|
||||
}
|
||||
|
||||
li.user-menu-tab {
|
||||
padding: 2px 10px;
|
||||
display: block;
|
||||
border: 1px solid #7777;
|
||||
border-bottom: none;
|
||||
}
|
||||
li.user-menu-tab:last-child {
|
||||
border-bottom: 1px solid #7777;
|
||||
}
|
||||
|
||||
li.user-menu-tab p,
|
||||
li.user-menu-tab a {
|
||||
display: block;
|
||||
padding: 2px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
li.user-menu-tab a:focus {
|
||||
outline: 1px solid #129fea;
|
||||
}
|
||||
|
||||
li.user-menu-tab.selected {
|
||||
background-color: #363636;
|
||||
font-weight: bold;
|
||||
border: 1px solid #b0b0b0;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* User "tab" (content container)
|
||||
*/
|
||||
|
||||
.user-tab {
|
||||
padding: 20px;
|
||||
border: 1px solid #7777;
|
||||
}
|
112
src/invidious/frontend/user_menu.cr
Normal file
112
src/invidious/frontend/user_menu.cr
Normal file
|
@ -0,0 +1,112 @@
|
|||
module Invidious::Frontend::UserMenu
|
||||
extend self
|
||||
|
||||
# -------------------
|
||||
# Menu items
|
||||
# -------------------
|
||||
|
||||
enum UserContentMenu
|
||||
Subscriptions
|
||||
WatchHistory
|
||||
Playlists
|
||||
end
|
||||
|
||||
enum UserAccountMenu
|
||||
Preferences
|
||||
Account
|
||||
ImportExport
|
||||
LogOut
|
||||
end
|
||||
|
||||
private alias UserMenuItem = UserContentMenu | UserAccountMenu
|
||||
|
||||
# -------------------
|
||||
# HTML templates
|
||||
# -------------------
|
||||
|
||||
# Generates the following menu:
|
||||
#
|
||||
# ```
|
||||
# <div class="user-menu"><ul>
|
||||
# <li class="user-menu-tab"><a href="#">Subscriptions</a></li>
|
||||
# <li class="user-menu-tab"><a href="#">Watch history</a></li>
|
||||
# <li class="user-menu-tab"><a href="#">Playlists</a></li>
|
||||
# </ul></div>
|
||||
#
|
||||
# <div class="user-menu"><ul>
|
||||
# <li class="user-menu-tab"><p>Preferences</p></li>
|
||||
# <li class="user-menu-tab"><a href="#">Account</a></li>
|
||||
# <li class="user-menu-tab"><a href="#">Import & Export</a></li>
|
||||
# <li class="user-menu-tab"><p>Log Out</p></li>
|
||||
# </ul></div>
|
||||
# ```
|
||||
#
|
||||
# The selected entry will have the "selected" class.
|
||||
#
|
||||
def make_menu(env : HTTP::Server::Context, selected_item : UserMenuItem) : String
|
||||
# A capacity of 1500 is enough to store the HTML (empty)
|
||||
# plus the URLs with parameters and the translated text.
|
||||
str_builder = String::Builder.new(1500)
|
||||
|
||||
# TODO: Get variables from HTTP env
|
||||
locale = env.get("preferences").as(Preferences).locale
|
||||
params = nil
|
||||
|
||||
# Start of menu #1
|
||||
str_builder << <<-HTML
|
||||
<div class="user-menu"><ul>
|
||||
HTML
|
||||
|
||||
# Menu items for the 1st menu
|
||||
UserContentMenu.each do |menu_item|
|
||||
case menu_item
|
||||
when .subscriptions? then url = "/subscription_manager"
|
||||
when .watch_history? then url = "/feed/history"
|
||||
when .playlists? then url = "/user/subscription_manager"
|
||||
end
|
||||
|
||||
url += "?" + params if params
|
||||
|
||||
text = HTML.escape(translate(locale, "user_menu_item_" + menu_item.to_s.underscore))
|
||||
|
||||
if menu_item == selected_item
|
||||
str_builder << "\t<li class=\"user-menu-tab selected\"><p>#{text}</p></li>\n"
|
||||
else
|
||||
str_builder << "\t<li class=\"user-menu-tab\"><a href=\"#{url}\">#{text}</a></li>\n"
|
||||
end
|
||||
end
|
||||
|
||||
# End of menu #1, start of menu #2
|
||||
str_builder << <<-HTML
|
||||
</ul></div>
|
||||
<div class="user-menu"><ul>
|
||||
HTML
|
||||
|
||||
# Menu items for the 2nd menu
|
||||
UserAccountMenu.each do |menu_item|
|
||||
case menu_item
|
||||
when .preferences? then url = "/preferences"
|
||||
when .account? then url = "/" # TODO
|
||||
when .import_export? then url = "/data_control"
|
||||
when .log_out? then url = "/log_out"
|
||||
end
|
||||
|
||||
url += "?" + params if params
|
||||
|
||||
text = HTML.escape(translate(locale, "user_menu_item_" + menu_item.to_s.underscore))
|
||||
|
||||
if menu_item == selected_item
|
||||
str_builder << "\t<li class=\"user-menu-tab selected\"><p>#{text}</p></li>\n"
|
||||
else
|
||||
str_builder << "\t<li class=\"user-menu-tab\"><a href=\"#{url}\">#{text}</a></li>\n"
|
||||
end
|
||||
end
|
||||
|
||||
# End of menu #2
|
||||
str_builder << <<-HTML
|
||||
</ul></div>
|
||||
HTML
|
||||
|
||||
return str_builder.to_s
|
||||
end
|
||||
end
|
|
@ -2,6 +2,43 @@ struct Preferences
|
|||
include JSON::Serializable
|
||||
include YAML::Serializable
|
||||
|
||||
# -------------------
|
||||
# Constants
|
||||
# -------------------
|
||||
|
||||
SPEEDS = {2.0, 1.75, 1.5, 1.25, 1.0, 0.75, 0.5, 0.25}
|
||||
|
||||
QUALITIES = {"dash", "hd720", "medium", "small"}
|
||||
|
||||
DASH_QUALITIES = {
|
||||
"auto", "best", "4320p", "2160p", "1440p", "1080p",
|
||||
"720p", "480p", "360p", "240p", "144p", "worst",
|
||||
}
|
||||
|
||||
COMMENT_SOURCES = {"none", "youtube", "reddit"}
|
||||
|
||||
THEMES = {"auto", "light", "dark"}
|
||||
PLAYER_STYLES = {"invidious", "youtube"}
|
||||
|
||||
FEED_OPTIONS = {"none", "Popular", "trending"}
|
||||
FEED_OPTIONS_USER = {"none", "Popular", "trending", "Subscriptions", "Playlists"}
|
||||
|
||||
HOMEPAGES = {"Search", "Popular", "trending"}
|
||||
HOMEPAGES_USER = {"Search", "Popular", "trending", "Subscriptions", "Playlists"}
|
||||
|
||||
SORT_OPTIONS = {
|
||||
"published",
|
||||
"published - reverse",
|
||||
"alphabetically",
|
||||
"alphabetically - reverse",
|
||||
"channel name",
|
||||
"channel name - reverse",
|
||||
}
|
||||
|
||||
# -------------------
|
||||
# Properties
|
||||
# -------------------
|
||||
|
||||
property annotations : Bool = CONFIG.default_user_preferences.annotations
|
||||
property annotations_subscribed : Bool = CONFIG.default_user_preferences.annotations_subscribed
|
||||
property autoplay : Bool = CONFIG.default_user_preferences.autoplay
|
||||
|
@ -56,6 +93,10 @@ struct Preferences
|
|||
property volume : Int32 = CONFIG.default_user_preferences.volume
|
||||
property save_player_pos : Bool = CONFIG.default_user_preferences.save_player_pos
|
||||
|
||||
# -------------------
|
||||
# Converter modules
|
||||
# -------------------
|
||||
|
||||
module BoolToString
|
||||
def self.to_json(value : String, json : JSON::Builder)
|
||||
json.string value
|
||||
|
|
|
@ -1,58 +1,70 @@
|
|||
<% content_for "header" do %>
|
||||
<title><%= translate(locale, "Import and Export Data") %> - Invidious</title>
|
||||
<link rel="stylesheet" href="/css/user.css?v=<%= ASSET_COMMIT %>">
|
||||
<% end %>
|
||||
|
||||
<div class="h-box">
|
||||
<form class="pure-form pure-form-aligned" enctype="multipart/form-data" action="/data_control?referer=<%= URI.encode_www_form(referer) %>" method="post">
|
||||
<fieldset>
|
||||
<legend><%= translate(locale, "Import") %></legend>
|
||||
<div class="h-box pure-g">
|
||||
<div class="pure-u-1 pure-u-md-1-5">
|
||||
<%= Invidious::Frontend::UserMenu.make_menu(env, :import_export) %>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="import_youtube"><%= translate(locale, "Import Invidious data") %></label>
|
||||
<input type="file" id="import_invidious" name="import_invidious">
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-md-4-5">
|
||||
<div class="user-tab">
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="import_youtube">
|
||||
<a rel="noopener" target="_blank" href="https://github.com/iv-org/documentation/blob/master/Export-YouTube-subscriptions.md">
|
||||
<%= translate(locale, "Import YouTube subscriptions") %>
|
||||
</a>
|
||||
</label>
|
||||
<input type="file" id="import_youtube" name="import_youtube">
|
||||
</div>
|
||||
<form class="pure-form pure-form-aligned" enctype="multipart/form-data" action="/data_control?referer=<%= URI.encode_www_form(referer) %>" method="post">
|
||||
<fieldset>
|
||||
<legend><%= translate(locale, "Import") %></legend>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="import_freetube"><%= translate(locale, "Import FreeTube subscriptions (.db)") %></label>
|
||||
<input type="file" id="import_freetube" name="import_freetube">
|
||||
</div>
|
||||
<div class="pure-control-group">
|
||||
<label for="import_youtube"><%= translate(locale, "Import Invidious data") %></label>
|
||||
<input type="file" id="import_invidious" name="import_invidious">
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="import_newpipe_subscriptions"><%= translate(locale, "Import NewPipe subscriptions (.json)") %></label>
|
||||
<input type="file" id="import_newpipe_subscriptions" name="import_newpipe_subscriptions">
|
||||
</div>
|
||||
<div class="pure-control-group">
|
||||
<label for="import_youtube">
|
||||
<a rel="noopener" target="_blank" href="https://github.com/iv-org/documentation/blob/master/Export-YouTube-subscriptions.md">
|
||||
<%= translate(locale, "Import YouTube subscriptions") %>
|
||||
</a>
|
||||
</label>
|
||||
<input type="file" id="import_youtube" name="import_youtube">
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="import_newpipe"><%= translate(locale, "Import NewPipe data (.zip)") %></label>
|
||||
<input type="file" id="import_newpipe" name="import_newpipe">
|
||||
</div>
|
||||
<div class="pure-control-group">
|
||||
<label for="import_freetube"><%= translate(locale, "Import FreeTube subscriptions (.db)") %></label>
|
||||
<input type="file" id="import_freetube" name="import_freetube">
|
||||
</div>
|
||||
|
||||
<div class="pure-controls">
|
||||
<button type="submit" class="pure-button pure-button-primary"><%= translate(locale, "Import") %></button>
|
||||
</div>
|
||||
<div class="pure-control-group">
|
||||
<label for="import_newpipe_subscriptions"><%= translate(locale, "Import NewPipe subscriptions (.json)") %></label>
|
||||
<input type="file" id="import_newpipe_subscriptions" name="import_newpipe_subscriptions">
|
||||
</div>
|
||||
|
||||
<legend><%= translate(locale, "Export") %></legend>
|
||||
<div class="pure-control-group">
|
||||
<label for="import_newpipe"><%= translate(locale, "Import NewPipe data (.zip)") %></label>
|
||||
<input type="file" id="import_newpipe" name="import_newpipe">
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<a href="/subscription_manager?action_takeout=1"><%= translate(locale, "Export subscriptions as OPML") %></a>
|
||||
</div>
|
||||
<div class="pure-controls">
|
||||
<button type="submit" class="pure-button pure-button-primary"><%= translate(locale, "Import") %></button>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<a href="/subscription_manager?action_takeout=1&format=newpipe"><%= translate(locale, "Export subscriptions as OPML (for NewPipe & FreeTube)") %></a>
|
||||
</div>
|
||||
<legend><%= translate(locale, "Export") %></legend>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<a href="/subscription_manager?action_takeout=1"><%= translate(locale, "Export subscriptions as OPML") %></a>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<a href="/subscription_manager?action_takeout=1&format=newpipe"><%= translate(locale, "Export subscriptions as OPML (for NewPipe & FreeTube)") %></a>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<a href="/subscription_manager?action_takeout=1&format=json"><%= translate(locale, "Export data as JSON") %></a>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<a href="/subscription_manager?action_takeout=1&format=json"><%= translate(locale, "Export data as JSON") %></a>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -40,20 +40,20 @@
|
|||
<div class="pure-control-group">
|
||||
<label for="speed"><%= translate(locale, "preferences_speed_label") %></label>
|
||||
<select name="speed" id="speed">
|
||||
<% {2.0, 1.75, 1.5, 1.25, 1.0, 0.75, 0.5, 0.25}.each do |option| %>
|
||||
<%- Preferences::SPEEDS.each do |option| -%>
|
||||
<option <% if preferences.speed == option %> selected <% end %>><%= option %></option>
|
||||
<% end %>
|
||||
<%- end -%>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="quality"><%= translate(locale, "preferences_quality_label") %></label>
|
||||
<select name="quality" id="quality">
|
||||
<% {"dash", "hd720", "medium", "small"}.each do |option| %>
|
||||
<% if !(option == "dash" && CONFIG.disabled?("dash")) %>
|
||||
<%- Preferences::QUALITIES.each do |option| -%>
|
||||
<%- if !(option == "dash" && CONFIG.disabled?("dash")) -%>
|
||||
<option value="<%= option %>" <% if preferences.quality == option %> selected <% end %>><%= translate(locale, "preferences_quality_option_" + option) %></option>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%- end -%>
|
||||
<%- end -%>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
@ -61,9 +61,9 @@
|
|||
<div class="pure-control-group">
|
||||
<label for="quality_dash"><%= translate(locale, "preferences_quality_dash_label") %></label>
|
||||
<select name="quality_dash" id="quality_dash">
|
||||
<% {"auto", "best", "4320p", "2160p", "1440p", "1080p", "720p", "480p", "360p", "240p", "144p", "worst"}.each do |option| %>
|
||||
<%- Preferences::DASH_QUALITIES.each do |option| -%>
|
||||
<option value="<%= option %>" <% if preferences.quality_dash == option %> selected <% end %>><%= translate(locale, "preferences_quality_dash_option_" + option) %></option>
|
||||
<% end %>
|
||||
<%- end -%>
|
||||
</select>
|
||||
</div>
|
||||
<% end %>
|
||||
|
@ -78,9 +78,9 @@
|
|||
<label for="comments[0]"><%= translate(locale, "preferences_comments_label") %></label>
|
||||
<% preferences.comments.each_with_index do |comments, index| %>
|
||||
<select name="comments[<%= index %>]" id="comments[<%= index %>]">
|
||||
<% {"", "youtube", "reddit"}.each do |option| %>
|
||||
<option value="<%= option %>" <% if preferences.comments[index] == option %> selected <% end %>><%= translate(locale, option.blank? ? "none" : option) %></option>
|
||||
<% end %>
|
||||
<%- Preferences::COMMENT_SOURCES.each do |option| -%>
|
||||
<option value="<%= option %>" <% if preferences.comments[index] == option %> selected <% end %>><%= translate(locale, option) %></option>
|
||||
<%- end -%>
|
||||
</select>
|
||||
<% end %>
|
||||
</div>
|
||||
|
@ -144,18 +144,18 @@
|
|||
<div class="pure-control-group">
|
||||
<label for="player_style"><%= translate(locale, "preferences_player_style_label") %></label>
|
||||
<select name="player_style" id="player_style">
|
||||
<% {"invidious", "youtube"}.each do |option| %>
|
||||
<%- Preferences::PLAYER_STYLES.each do |option| -%>
|
||||
<option value="<%= option %>" <% if preferences.player_style == option %> selected <% end %>><%= translate(locale, option) %></option>
|
||||
<% end %>
|
||||
<%- end -%>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="dark_mode"><%= translate(locale, "preferences_dark_mode_label") %></label>
|
||||
<select name="dark_mode" id="dark_mode">
|
||||
<% {"", "light", "dark"}.each do |option| %>
|
||||
<option value="<%= option %>" <% if preferences.dark_mode == option %> selected <% end %>><%= translate(locale, option.blank? ? "auto" : option) %></option>
|
||||
<% end %>
|
||||
<%- Preferences::THEMES.each do |option| -%>
|
||||
<option value="<%= option %>" <% if preferences.dark_mode == option %> selected <% end %>><%= translate(locale, option) %></option>
|
||||
<%- end -%>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
@ -164,30 +164,34 @@
|
|||
<input name="thin_mode" id="thin_mode" type="checkbox" <% if preferences.thin_mode %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
<% if env.get?("user") %>
|
||||
<% feed_options = {"", "Popular", "Trending", "Subscriptions", "Playlists"} %>
|
||||
<% else %>
|
||||
<% feed_options = {"", "Popular", "Trending"} %>
|
||||
<% end %>
|
||||
<%-
|
||||
if env.get?("user")
|
||||
feed_options = Preferences::FEED_OPTIONS_USER
|
||||
homepages = Preferences::HOMEPAGES_USER
|
||||
else
|
||||
feed_options = Preferences::FEED_OPTIONS
|
||||
homepages = Preferences::HOMEPAGES
|
||||
end
|
||||
-%>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="default_home"><%= translate(locale, "preferences_default_home_label") %></label>
|
||||
<select name="default_home" id="default_home">
|
||||
<% feed_options.each do |option| %>
|
||||
<option value="<%= option %>" <% if preferences.default_home == option %> selected <% end %>><%= translate(locale, option.blank? ? "Search" : option) %></option>
|
||||
<% end %>
|
||||
<%- homepages.each do |option| -%>
|
||||
<option value="<%= option %>" <% if preferences.default_home == option %> selected <% end %>><%= translate(locale, option) %></option>
|
||||
<%- end -%>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="feed_menu"><%= translate(locale, "preferences_feed_menu_label") %></label>
|
||||
<% (feed_options.size - 1).times do |index| %>
|
||||
<%- (feed_options.size - 1).times do |index| -%>
|
||||
<select name="feed_menu[<%= index %>]" id="feed_menu[<%= index %>]">
|
||||
<% feed_options.each do |option| %>
|
||||
<option value="<%= option %>" <% if preferences.feed_menu[index]? == option %> selected <% end %>><%= translate(locale, option.blank? ? "Search" : option) %></option>
|
||||
<% end %>
|
||||
<%- feed_options.each do |option| -%>
|
||||
<option value="<%= option %>" <% if preferences.feed_menu[index]? == option %> selected <% end %>><%= translate(locale, option) %></option>
|
||||
<%- end -%>
|
||||
</select>
|
||||
<% end %>
|
||||
<%- end -%>
|
||||
</div>
|
||||
<% if env.get? "user" %>
|
||||
<div class="pure-control-group">
|
||||
|
@ -224,9 +228,9 @@
|
|||
<div class="pure-control-group">
|
||||
<label for="sort"><%= translate(locale, "preferences_sort_label") %></label>
|
||||
<select name="sort" id="sort">
|
||||
<% {"published", "published - reverse", "alphabetically", "alphabetically - reverse", "channel name", "channel name - reverse"}.each do |option| %>
|
||||
<%- Preferences::SORT_OPTIONS.each do |option| -%>
|
||||
<option value="<%= option %>" <% if preferences.sort == option %> selected <% end %>><%= translate(locale, option) %></option>
|
||||
<% end %>
|
||||
<%- end -%>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
@ -263,21 +267,21 @@
|
|||
<div class="pure-control-group">
|
||||
<label for="admin_default_home"><%= translate(locale, "preferences_default_home_label") %></label>
|
||||
<select name="admin_default_home" id="admin_default_home">
|
||||
<% feed_options.each do |option| %>
|
||||
<option value="<%= option %>" <% if CONFIG.default_user_preferences.default_home == option %> selected <% end %>><%= translate(locale, option.blank? ? "none" : option) %></option>
|
||||
<% end %>
|
||||
<%- homepages.each do |option| -%>
|
||||
<option value="<%= option %>" <% if CONFIG.default_user_preferences.default_home == option %> selected <% end %>><%= translate(locale, option) %></option>
|
||||
<%- end -%>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="admin_feed_menu"><%= translate(locale, "preferences_feed_menu_label") %></label>
|
||||
<% (feed_options.size - 1).times do |index| %>
|
||||
<%- (feed_options.size - 1).times do |index| -%>
|
||||
<select name="admin_feed_menu[<%= index %>]" id="admin_feed_menu[<%= index %>]">
|
||||
<% feed_options.each do |option| %>
|
||||
<option value="<%= option %>" <% if CONFIG.default_user_preferences.feed_menu[index]? == option %> selected <% end %>><%= translate(locale, option.blank? ? "none" : option) %></option>
|
||||
<% end %>
|
||||
<%- feed_options.each do |option| -%>
|
||||
<option value="<%= option %>" <% if CONFIG.default_user_preferences.feed_menu[index]? == option %> selected <% end %>><%= translate(locale, option) %></option>
|
||||
<%- end -%>
|
||||
</select>
|
||||
<% end %>
|
||||
<%- end -%>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue