Multi frontend: listings, sidebar, and editing UI.

This commit is contained in:
Max Goodman
2013-03-09 01:49:59 -08:00
parent c564d16159
commit b4b8df9d40
18 changed files with 962 additions and 22 deletions

View File

@@ -128,6 +128,11 @@ def make_map():
where='overview')
mc('/u/:username', controller='redirect', action='user_redirect')
mc('/user/:username/m/:multi', controller='hot', action='listing')
mc('/user/:username/m/:multi/new', controller='new', action='listing')
mc('/user/:username/m/:multi/:sort', controller='browse', sort='top',
action='listing', requirements=dict(sort='top|controversial'))
# preserve timereddit URLs from 4/1/2012
mc('/t/:timereddit', controller='redirect', action='timereddit_redirect')
mc('/t/:timereddit/*rest', controller='redirect',

View File

@@ -77,6 +77,7 @@ class ListingController(RedditController, OAuth2ResourceController):
# login box, subreddit box, submit box, etc, visible
show_sidebar = True
show_chooser = False
# class (probably a subclass of Reddit) to use to render the page.
render_cls = Reddit
@@ -112,14 +113,22 @@ class ListingController(RedditController, OAuth2ResourceController):
if self.bare:
return responsive(content.render())
else:
return self.render_cls(content=content,
page_classes=self.extra_page_classes,
show_sidebar=self.show_sidebar,
nav_menus=self.menus,
title=self.title(),
robots=getattr(self, "robots", None),
**self.render_params).render()
page_classes = self.extra_page_classes
if (self.show_chooser and
c.user_is_loggedin and c.user.pref_show_left_bar and
isinstance(c.site, (DefaultSR, AllSR, LabeledMulti))):
page_classes = page_classes + ['with-listing-chooser']
content = PaneStack([ListingChooser(), content])
return self.render_cls(content=content,
page_classes=page_classes,
show_sidebar=self.show_sidebar,
nav_menus=self.menus,
title=self.title(),
robots=getattr(self, "robots", None),
**self.render_params).render()
def content(self):
"""Renderable object which will end up as content of the render_cls"""
@@ -225,6 +234,7 @@ class FixListing(object):
class HotController(FixListing, ListingController):
where = 'hot'
extra_page_classes = ListingController.extra_page_classes + ['hot-page']
show_chooser = True
def make_requested_ad(self):
try:
@@ -395,6 +405,7 @@ class NewController(ListingController):
where = 'new'
title_text = _('newest submissions')
extra_page_classes = ListingController.extra_page_classes + ['new-page']
show_chooser = True
def keep_fn(self):
def keep(item):
@@ -443,6 +454,7 @@ class RisingController(NewController):
class BrowseController(ListingController):
where = 'browse'
show_chooser = True
def keep_fn(self):
"""For merged time-listings, don't show items that are too old

View File

@@ -101,6 +101,7 @@ class PostController(ApiController):
pref_show_adbox = VBoolean("show_adbox"),
pref_show_sponsors = VBoolean("show_sponsors"),
pref_show_sponsorships = VBoolean("show_sponsorships"),
pref_show_left_bar = VBoolean("show_left_bar"),
pref_highlight_new_comments = VBoolean("highlight_new_comments"),
pref_monitor_mentions=VBoolean("monitor_mentions"),
all_langs = VOneOf('all-langs', ('all', 'some'), default='all'))

View File

@@ -91,6 +91,7 @@ from r2.models import (
FakeSubreddit,
Friends,
Frontpage,
LabeledMulti,
Link,
MultiReddit,
NotFound,
@@ -103,6 +104,7 @@ from r2.models import (
valid_feed,
valid_otp_cookie,
)
from r2.lib.db import tdb_cassandra
NEVER = datetime(2037, 12, 31, 23, 59, 59)
@@ -386,6 +388,14 @@ def set_subreddit():
elif not c.error_page and not request.path.startswith("/api/login/") :
abort(404)
routes_dict = request.environ["pylons.routes_dict"]
if "multi" in routes_dict and "username" in routes_dict:
try:
path = '/user/%s/m/%s' % (routes_dict["username"], routes_dict["multi"])
c.site = LabeledMulti._byID(path)
except tdb_cassandra.NotFound:
abort(404)
#if we didn't find a subreddit, check for a domain listing
if not sr_name and isinstance(c.site, DefaultSR) and domain:
c.site = DomainSR(domain)
@@ -1068,13 +1078,19 @@ class RedditController(MinimalController):
# check if the user has access to this subreddit
if not c.site.can_view(c.user) and not c.error_page:
public_description = c.site.public_description
errpage = pages.RedditError(strings.private_subreddit_title,
strings.private_subreddit_message,
image="subreddit-private.png",
sr_description=public_description)
request.environ['usable_error_content'] = errpage.render()
self.abort403()
if isinstance(c.site, LabeledMulti):
# do not leak the existence of multis via 403.
self.abort404()
else:
public_description = c.site.public_description
errpage = pages.RedditError(
strings.private_subreddit_title,
strings.private_subreddit_message,
image="subreddit-private.png",
sr_description=public_description,
)
request.environ['usable_error_content'] = errpage.render()
self.abort403()
#check over 18
if (c.site.over_18 and not c.over18 and

View File

@@ -376,6 +376,7 @@ module["reddit"] = LocalizedModule("reddit.js",
"wiki.js",
"apps.js",
"gold.js",
"multi.js",
)
module["admin"] = Module("admin.js",

View File

@@ -25,7 +25,7 @@ from collections import OrderedDict
from r2.lib.wrapped import Wrapped, Templated, CachedTemplate
from r2.models import Account, FakeAccount, DefaultSR, make_feedurl
from r2.models import FakeSubreddit, Subreddit, SubSR, AllMinus, AllSR
from r2.models import Friends, All, Sub, NotFound, DomainSR, Random, Mod, RandomNSFW, RandomSubscription, MultiReddit, ModSR, Frontpage
from r2.models import Friends, All, Sub, NotFound, DomainSR, Random, Mod, RandomNSFW, RandomSubscription, MultiReddit, ModSR, Frontpage, LabeledMulti
from r2.models import Link, Printable, Trophy, bidding, PromoCampaign, PromotionWeights, Comment
from r2.models import Flair, FlairTemplate, FlairTemplateBySubredditIndex
from r2.models import USER_FLAIR, LINK_FLAIR
@@ -91,7 +91,7 @@ except ImportError:
def ips_by_account_id(account_id):
return []
from things import wrap_links, default_thing_wrapper
from things import wrap_links, wrap_things, default_thing_wrapper
datefmt = _force_utf8(_('%d %b %Y'))
@@ -350,19 +350,24 @@ class Reddit(Templated):
if c.user.pref_show_sponsorships or not c.user.gold:
ps.append(SponsorshipBox())
if isinstance(c.site, (MultiReddit, ModSR)) and c.user_is_loggedin:
if isinstance(c.site, (MultiReddit, ModSR)):
srs = Subreddit._byID(c.site.sr_ids, data=True,
return_dict=False)
if c.user_is_admin or c.site.is_moderator(c.user):
ps.append(self.sr_admin_menu())
if srs:
if isinstance(c.site, LabeledMulti):
ps.append(MultiInfoBar(c.site, srs, c.user))
c.js_preload.set_wrapped(
'/api/multi/%s' % c.site.path.lstrip('/'), c.site)
elif srs:
if isinstance(c.site, ModSR):
box = SubscriptionBox(srs, multi_text=strings.mod_multi)
else:
box = SubscriptionBox(srs)
ps.append(SideContentBox(_('these subreddits'), [box]))
if c.user_is_admin or c.site.is_moderator(c.user):
ps.append(self.sr_admin_menu())
if isinstance(c.site, AllSR):
ps.append(AllInfoBar(c.site, c.user))
@@ -1801,6 +1806,15 @@ class SubredditTopBar(CachedTemplate):
return menus
class MultiInfoBar(Templated):
def __init__(self, multi, srs, user):
Templated.__init__(self)
self.multi = wrap_things(multi)[0]
self.can_edit = multi.can_edit(user)
self.srs = srs
class SubscriptionBox(Templated):
"""The list of reddits a user is currently subscribed to to go in
the right pane."""
@@ -4062,6 +4076,38 @@ class ModeratorPermissions(Templated):
Templated.__init__(self, permissions_type=permissions_type,
editable=editable, embedded=embedded)
class ListingChooser(Templated):
def __init__(self):
Templated.__init__(self)
self.sections = defaultdict(list)
self.add_item("global", _("subscribed"), '/',
description=_("your front page"))
self.add_item("other", _("everything"), '/r/all',
description=_("from all subreddits"))
if c.user_is_loggedin:
multis = LabeledMulti.by_owner(c.user)
multis.sort(key=lambda multi: multi.name)
for multi in multis:
self.add_item("multi", multi.name, multi.path)
self.selected_item = self.find_selected()
self.selected_item["selected"] = True
def add_item(self, section, name, path, description=None):
self.sections[section].append({
"name": name,
"description": description,
"path": path,
"selected": False,
})
def find_selected(self):
path = request.path
matching = [item for item in chain(*self.sections.values())
if path.startswith(item["path"]) or
c.site.path.startswith(item["path"])]
matching.sort(key=lambda item: len(item["path"]), reverse=True)
return matching[0]
class PolicyView(Templated):
pass

View File

@@ -209,6 +209,10 @@ Note: there are a couple of places outside of your subreddit where someone can c
all_msg=_("full permissions"),
none_msg=_("no permissions"),
),
categorize = _('categorize'),
are_you_sure = _('are you sure?'),
yes = _('yes'),
no = _('no'),
)
class StringHandler(object):

View File

@@ -88,6 +88,7 @@ class Account(Thing):
pref_show_adbox = True,
pref_show_sponsors = True, # sponsored links
pref_show_sponsorships = True,
pref_show_left_bar = True,
pref_highlight_new_comments = True,
pref_monitor_mentions=True,
mobile_compress = False,

BIN
r2/r2/public/static/add.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 B

View File

@@ -1,3 +1,17 @@
.box-sizing(@sizing) {
box-sizing: @sizing;
-webkit-box-sizing: @sizing;
-moz-box-sizing: @sizing;
}
.no-select {
-webkit-user-select: none;
-moz-user-select: none;
-o-user-select: none;
-ms-user-select: none;
user-select: none;
}
html {
height: 100%; /* Needed for toolbar's comments panel's pinstripe */
}
@@ -1020,6 +1034,77 @@ a.author { margin-right: 0.5em; }
}
}
.hover-bubble.multi-selector {
@arrow-offset: 40px;
margin-top: -7px - @arrow-offset;
min-width: 130px;
min-height: @arrow-offset * 2;
padding: 8px 0;
.no-select;
&:before, &:after {
top: 8px + @arrow-offset;
}
strong, a.sr {
display: block;
margin: 3px 0;
text-align: center;
}
strong {
font-size: 1.05em;
font-weight: bold;
color: #333;
}
.throbber {
position: absolute;
top: 10px;
right: 8px;
}
.multi-list {
margin-top: 5px;
}
label {
font-size: 1.25em;
display: block;
padding: 5px 12px;
&:hover {
background: #eee;
}
input[type="checkbox"] {
margin-top: 0;
vertical-align: middle;
}
a {
float: right;
margin-left: 7px;
width: 12px;
height: 12px;
line-height: 12px;
background: white;
border: 1px solid lighten(#369, 20%);
border-radius: 2px;
text-align: center;
opacity: .65;
&:hover {
opacity: 1;
}
}
}
input {
margin-right: 5px;
}
}
.infotext {
border: 1px solid #369;
background-color: #EFF7FF;
@@ -4998,6 +5083,135 @@ table.calendar {
margin-left: 5px;
}
.confirm-button .confirmation {
color: red;
white-space: nowrap;
.prompt {
margin-right: .5em;
}
}
.gray-buttons {
button {
padding: 0;
margin: 0;
border: none;
background: none;
color: #888;
font-weight: bold;
&:hover {
text-decoration: underline;
}
}
}
.multi-details {
h1 {
margin-bottom: 0;
}
h1 a, .throbber {
vertical-align: middle;
}
.throbber {
margin-left: 5px;
}
h2 {
margin-top: 0;
margin-bottom: 3px;
}
.settings {
margin-bottom: 5px;
input[type="radio"] {
margin: 0;
margin-right: 3px;
vertical-align: middle;
}
label {
margin-right: 10px;
}
.delete {
margin-left: 5px;
}
}
h3 {
font-weight: normal;
color: #777;
margin-bottom: 3px;
}
ul, form.add-sr {
margin-left: 12px;
}
button.remove-sr, button.add {
.box-sizing(content-box);
text-indent: -9999px;
margin-left: 3px;
background: none no-repeat;
border: 3px solid transparent;
padding: 0;
opacity: .3;
&:hover {
opacity: 1;
}
&:active {
position: relative;
top: 1px;
}
&.remove-sr {
width: 9px;
height: 9px;
background-image: url(../close-small.png); /* SPRITE */
}
&.add {
width: 15px;
height: 15px;
background-image: url(../add.png); /* SPRITE */
}
}
form.add-sr {
.sr-name, button.add {
vertical-align: middle;
}
.sr-name {
border: 1px solid #ccc;
padding: 3px;
}
button.add {
border: 5px solid transparent;
}
}
li {
font-size: 1.15em;
line-height: 1.5em;
a, button {
vertical-align: middle;
}
}
.bottom {
margin-top: 1em;
}
}
.sidecontentbox {
font-size: normal;
}
@@ -6712,3 +6926,212 @@ body.gold .buttons li.comment-save-button { display: inline; }
text-align: left;
white-space: normal;
}
body.with-listing-chooser {
position: relative;
& #header .tabmenu {
position: absolute;
margin-left: -2px;
left: 130px;
bottom: 0;
li:first-child.selected {
margin-left: 2px;
}
}
& #header .pagename {
position: absolute;
left: 130px;
bottom: 20px;
}
& > .content, & .footer-parent {
margin-left: 140px
}
}
.listing-chooser {
@width: 130px;
@shadow-width: 4px;
position: absolute;
top: 65px;
left: 0;
bottom: 0;
width: @width;
border-right: 1px solid #ccc;
background: #f7f7f7;
&:after {
content: '';
position: absolute;
right: 0;
top: 0;
bottom: 0;
left: @width - @shadow-width;
box-shadow: -@shadow-width 0 2*@shadow-width -@shadow-width rgba(0, 0, 0, .3) inset;
z-index: 90;
}
h3 {
color: #777;
text-align: right;
padding: 4px;
}
ul.global, ul.other {
padding: 8px 0;
li {
margin-left: 4px;
a {
font-size: 1.3em;
padding: 1em 5px;
padding-left: 12px;
}
}
}
ul.other {
margin-top: 10px;
}
ul.multis {
li {
margin-left: 12px;
a {
font-size: 1.2em;
padding: .8em 5px;
padding-left: 10px;
}
}
}
li {
text-align: left;
margin-bottom: 3px;
background: #fff;
border: 1px solid #ccc;
border-bottom-width: 2px;
border-right: none;
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
a {
display: block;
position: relative;
overflow: hidden;
text-overflow: ellipsis;
margin-right: 5px;
.description {
color: gray;
font-size: .8em;
font-weight: normal;
}
}
&:last-child a {
border-bottom: none;
}
&.selected {
position: relative;
background: lighten(#cee3f8, 6%);
border-color: lighten(#369, 40%);
z-index: 100;
a {
font-weight: bold;
}
&:after,
&:before {
position: absolute;
top: 50%;
margin-top: -15px;
display: block;
content: '';
border: 15px solid transparent;
border-style: solid solid outset; // mitigates firefox drawing a thicker arrow
z-index: 100;
}
&:before {
right: 0px;
border-right-color: #ccc;
}
&:after {
right: -2px;
border-right-color: white;
}
}
}
.create {
padding: 5px;
input[type="text"] {
width: 95px;
background: white;
border: 1px solid #ccc;
padding: 2px 5px;
margin-bottom: 3px;
display: none;
}
.error {
margin: 4px 0;
}
button {
display: inline;
text-align: center;
padding: 1px 4px;
margin: 0;
background: none;
border: 1px solid #777;
border-radius: 3px;
opacity: .5;
&:hover {
opacity: .90;
}
&:active {
background: #e9e9e9;
}
}
button, .throbber {
vertical-align: middle;
}
.throbber {
float: right;
}
&.expanded {
input[type="text"] {
display: block;
}
button {
opacity: .75;
background: #fcfcfc;
box-shadow: 0 1px 1px rgba(0, 0, 0, .25);
&:active {
position: relative;
top: 1px;
box-shadow: none;
}
}
}
}
}

View File

@@ -19,12 +19,21 @@ r.setup = function(config) {
r.setupBackbone = function() {
Backbone.ajax = function(request) {
var preloaded = r.preload.read(request.url)
var url = request.url,
preloaded = r.preload.read(url)
if (preloaded != null) {
request.success(preloaded)
return
}
var isLocal = url && (url[0] == '/' || url.lastIndexOf(r.config.currentOrigin, 0) == 0)
if (isLocal) {
if (!request.headers) {
request.headers = {}
}
request.headers['X-Modhash'] = r.config.modhash
}
return Backbone.$.ajax(request)
}
}
@@ -39,4 +48,5 @@ $(function() {
r.apps.init()
r.wiki.init()
r.gold.init()
r.multi.init()
})

View File

@@ -0,0 +1,240 @@
r.multi = {
init: function() {
this.mine = new r.multi.MyMultiCollection()
var detailsEl = $('.multi-details')
if (detailsEl.length) {
var multi = new r.multi.MultiReddit({
path: detailsEl.data('path')
})
multi.fetch()
new r.multi.MultiDetails({
model: multi,
el: detailsEl
})
}
var subscribeBubbleGroup = {}
$('.subscribe-button').each(function(idx, el) {
new r.multi.SubscribeButton({
el: el,
bubbleGroup: subscribeBubbleGroup
})
})
$('.listing-chooser').each(function(idx, el) {
new r.multi.ListingChooser({el: el})
})
}
}
r.multi.MultiRedditList = Backbone.Collection.extend({
model: Backbone.Model.extend({
idAttribute: 'name'
})
})
r.multi.MultiReddit = Backbone.Model.extend({
idAttribute: 'path',
url: function() {
return r.utils.joinURLs('/api/multi', this.id)
},
initialize: function() {
this.subreddits = new r.multi.MultiRedditList(this.get('subreddits'))
this.subreddits.url = this.url() + '/r/'
this.on('change:subreddits', function(model, value) {
this.subreddits.reset(value)
}, this)
this.subreddits.on('request', function(model, xhr, options) {
this.trigger('request', model, xhr, options)
}, this)
},
parse: function(response) {
return response.data
},
addSubreddit: function(name, options) {
this.subreddits.create({name: name}, options)
},
removeSubreddit: function(name, options) {
this.subreddits.get(name).destroy(options)
}
})
r.multi.MyMultiCollection = Backbone.Collection.extend({
url: '/api/multi/mine',
model: r.multi.MultiReddit,
create: function(attributes, options) {
if ('name' in attributes) {
attributes['path'] = '/user/' + r.config.logged + '/m/' + attributes['name']
delete attributes['name']
}
Backbone.Collection.prototype.create.call(this, attributes, options)
}
})
r.multi.MultiDetails = Backbone.View.extend({
itemTemplate: _.template('<li data-name="<%= name %>"><a href="/r/<%= name %>">/r/<%= name %></a><button class="remove-sr">x</button></li>'),
events: {
'submit .add-sr': 'addSubreddit',
'click .remove-sr': 'removeSubreddit',
'change [name="visibility"]': 'setVisibility',
'confirm .delete': 'deleteMulti'
},
initialize: function() {
this.model.subreddits.on('add remove', this.render, this)
this.model.on('request', function(model, xhr) {
r.ui.showWorkingDeferred(this.$el, xhr)
}, this)
new r.ui.ConfirmButton({el: this.$('button.delete')})
},
render: function() {
var srList = this.$('.subreddits')
srList.empty()
this.model.subreddits.each(function(sr) {
srList.append(this.itemTemplate({
name: sr.get('name')
}))
}, this)
},
addSubreddit: function(ev) {
ev.preventDefault()
var nameEl = this.$('.add-sr .sr-name'),
srName = $.trim(nameEl.val())
if (!srName) {
return
}
nameEl.val('')
this.$('.add-error').css('visibility', 'hidden')
this.model.addSubreddit(srName, {
wait: true,
success: _.bind(function() {
r.ui.refreshListing()
this.$('.add-error').hide()
}, this),
error: _.bind(function(model, xhr) {
var resp = JSON.parse(xhr.responseText)
this.$('.add-error')
.text(resp.explanation)
.css('visibility', 'visible')
.show()
}, this)
})
},
removeSubreddit: function(ev) {
var srName = $(ev.target).parent().data('name')
this.model.removeSubreddit(srName, {success: r.ui.refreshListing})
},
setVisibility: function() {
this.model.save({
visibility: this.$('[name="visibility"]:checked').val()
})
},
deleteMulti: function() {
this.model.destroy({
success: function() {
window.location = '/'
}
})
}
})
r.multi.SubscribeButton = Backbone.View.extend({
initialize: function() {
this.bubble = new r.multi.MultiSubscribeBubble({
parent: this.$el,
group: this.options.bubbleGroup,
sr_name: this.$el.data('sr_name')
})
this.$el.append(this.bubble.el)
}
})
r.multi.MultiSubscribeBubble = r.ui.Bubble.extend({
className: 'multi-selector hover-bubble anchor-right',
template: _.template('<div class="title"><strong><%= title %></strong><a class="sr" href="/r/<%= sr_name %>">/r/<%= sr_name %></a></div><div class="throbber"></div>'),
itemTemplate: _.template('<label><input type="checkbox" data-path="<%= path %>" <%= checked %>><%= name %><a href="<%= path %>" target="_blank">&rsaquo;</a></label>'),
events: {
'click input': 'toggleSubscribed'
},
initialize: function() {
this.on('show', this.load, this)
this.listenTo(r.multi.mine, 'reset', this.render)
r.ui.Bubble.prototype.initialize.apply(this)
},
load: function() {
r.ui.showWorkingDeferred(this.$el, r.multi.mine.fetch())
},
render: function() {
this.$el.html(this.template({
title: r.strings('categorize'),
sr_name: this.options.sr_name
}))
var content = $('<div class="multi-list">')
r.multi.mine.each(function(multi) {
content.append(this.itemTemplate({
name: multi.get('name'),
path: multi.get('path'),
checked: multi.subreddits.get(this.options.sr_name)
? 'checked' : ''
}))
}, this)
this.$el.append(content)
},
toggleSubscribed: function(ev) {
var checkbox = $(ev.target),
multi = r.multi.mine.get(checkbox.data('path'))
if (checkbox.is(':checked')) {
multi.addSubreddit(this.options.sr_name)
} else {
multi.removeSubreddit(this.options.sr_name)
}
}
})
r.multi.ListingChooser = Backbone.View.extend({
events: {
'submit .create': 'createClick'
},
createClick: function(ev) {
ev.preventDefault()
if (!this.$('.create').is('.expanded')) {
this.$('.create').addClass('expanded')
this.$('.create input[type="text"]').focus()
} else {
var name = this.$('.create input[type="text"]').val()
name = $.trim(name)
if (name) {
r.multi.mine.create({name: name}, {
success: function(multi) {
window.location = multi.get('path')
},
error: _.bind(function(multi, xhr) {
var resp = JSON.parse(xhr.responseText)
this.$('.error').text(resp.explanation).show()
}, this),
beforeSend: _.bind(r.ui.showWorkingDeferred, this, this.$el)
})
}
}
}
})

View File

@@ -557,3 +557,40 @@ r.ui.scrollFixed.prototype = {
}
}
}
r.ui.ConfirmButton = Backbone.View.extend({
confirmTemplate: _.template('<span class="confirmation"><span class="prompt"><%= are_you_sure %></span><button class="yes"><%= yes %></button> / <button class="no"><%= no %></button></div>'),
events: {
'click': 'click'
},
initialize: function() {
// wrap the specified element in a <span> and move its classes over to
// the wrapper. this is intended for progressive enhancement of a bare
// <button> element.
this.$target = this.$el
this.$target.wrap('<span>')
this.setElement(this.$target.parent())
this.$el
.attr('class', this.$target.attr('class'))
.addClass('confirm-button')
this.$target.attr('class', null)
},
click: function(ev) {
var target = $(ev.target)
if (this.$target.is(target)) {
this.$target.hide()
this.$el.append(this.confirmTemplate({
are_you_sure: r.strings('are_you_sure'),
yes: r.strings('yes'),
no: r.strings('no')
}))
} else if (target.is('.no')) {
this.$('.confirmation').remove()
this.$target.show()
} else if (target.is('.yes')) {
this.$target.trigger('confirm')
}
}
})

View File

@@ -7,6 +7,15 @@ r.utils = {
return r.config.static_root + '/' + item
},
joinURLs: function(/* arguments */) {
return _.map(arguments, function(url, idx) {
if (idx > 0 && url && url[0] != '/') {
url = '/' + url
}
return url
}).join('')
},
querySelectorFromEl: function(targetEl, selector) {
return $(targetEl).parents().andSelf()
.filter(selector || '*')

View File

@@ -0,0 +1,60 @@
## The contents of this file are subject to the Common Public Attribution
## License Version 1.0. (the "License"); you may not use this file except in
## compliance with the License. You may obtain a copy of the License at
## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
## License Version 1.1, but Sections 14 and 15 have been added to cover use of
## software over a computer network and provide for limited attribution for the
## Original Developer. In addition, Exhibit A has been modified to be
## consistent with Exhibit B.
##
## Software distributed under the License is distributed on an "AS IS" basis,
## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
## the specific language governing rights and limitations under the License.
##
## The Original Code is reddit.
##
## The Original Developer is the Initial Developer. The Initial Developer of
## the Original Code is reddit Inc.
##
## All portions of the code written by reddit are Copyright (c) 2006-2012
## reddit Inc. All Rights Reserved.
###############################################################################
<%def name="section_items(itemlist)">
%for item in itemlist:
<li
%if item['selected']:
class="selected"
%endif
>
<a href="${item['path']}">
${item['name']}
%if 'description' in item:
<br><span class="description">${item['description']}</span>
%endif
</a>
</li>
%endfor
</%def>
<div class="listing-chooser">
<ul class="global">
${section_items(thing.sections['global'])}
</ul>
<h3>${_('multireddits')}</h3>
<ul class="multis">
${section_items(thing.sections['multi'])}
<li class="create">
<form>
<input type="text" placeholder="${_('name')}"></input>
<div class="error"></div>
<button>${_('create')}</button><div class="throbber"></div>
</form>
</li>
</ul>
<ul class="other">
${section_items(thing.sections['other'])}
</ul>
</div>

View File

@@ -0,0 +1,73 @@
## The contents of this file are subject to the Common Public Attribution
## License Version 1.0. (the "License"); you may not use this file except in
## compliance with the License. You may obtain a copy of the License at
## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
## License Version 1.1, but Sections 14 and 15 have been added to cover use of
## software over a computer network and provide for limited attribution for the
## Original Developer. In addition, Exhibit A has been modified to be
## consistent with Exhibit B.
##
## Software distributed under the License is distributed on an "AS IS" basis,
## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
## the specific language governing rights and limitations under the License.
##
## The Original Code is reddit.
##
## The Original Developer is the Initial Developer. The Initial Developer of
## the Original Code is reddit Inc.
##
## All portions of the code written by reddit are Copyright (c) 2006-2012
## reddit Inc. All Rights Reserved.
###############################################################################
<%!
from r2.lib.strings import strings, Score
from r2.lib.pages import WrappedUser, ModList
%>
<%namespace file="utils.html" import="plain_link, thing_timestamp, text_with_links"/>
<%namespace file="printablebuttons.html" import="ynbutton, state_button" />
<div class="titlebox multi-details" data-path="${thing.multi.path}">
<h1 class="hover redditname">
<a href="${thing.multi.path}">${_('%s subreddits') % thing.multi.name}</a><div class="throbber"></div>
</h1>
<h2><a href="/user/${thing.multi.owner.name}">${_('curated by /u/%s') % thing.multi.owner.name}</a></h2>
%if thing.can_edit:
<div class="gray-buttons settings">
<label><input type="radio" name="visibility" value="private" ${'checked' if thing.multi.visibility == 'private' else ''}>${_('private')}</label>
<label><input type="radio" name="visibility" value="public" ${'checked' if thing.multi.visibility == 'public' else ''}>${_('public')}</label>
<button class="delete">${_('delete')}</button>
</div>
%endif
<h3>${_('subreddits in this multi:')}</h3>
<ul class="subreddits">
%for sr in thing.srs:
<li data-name="${sr.name}">
<a href="/r/${sr.name}">/r/${sr.name}</a>
%if thing.can_edit:
<button class="remove-sr">${_('remove')}</button>
%endif
</li>
%endfor
</ul>
%if thing.can_edit:
<form class="add-sr">
<input type="text" class="sr-name" placeholder="${_('add subreddit')}"><button class="add">${_('add')}</button>
<div class="error add-error"></div>
</form>
%endif
<div class="bottom">
%if thing.multi.owner:
${unsafe(_("created by&#32;%(user)s") % dict(user = unsafe(WrappedUser(thing.multi.owner).render())))}
%endif
<span class="age">
${_("a multireddit for")}&#32;${thing_timestamp(thing.multi)}
</span>
</div>
</div>

View File

@@ -232,6 +232,8 @@
<td class="prefright">
${checkbox(_("allow subreddits to show me custom styles"), "show_stylesheets")}
<br/>
${checkbox(_("show left sidebar"), "show_left_bar")}
<br/>
${checkbox(_("show user flair"), "show_flair")}
<br/>
${checkbox(_("show link flair"), "show_link_flair")}