8078ff8755
Certification Required page: * Show helpful advice if the reason for the page is only that the user had deleted their default profile pic, but their account was certified. Batch Photo Delete & Visibility: * On user galleries, owners and admins can batch Delete or Set Visibility on many photos at once. Checkboxes appear in the edit/delete row of each photo, and bulk actions appear at the bottom of the page along with select/unselect all boxes. * Deprecated the old /photo/delete endpoint: it now redirects to the batch delete page with the one photo ID. Misc Changes: * Notifications now sort unread to the top always.
953 lines
46 KiB
HTML
953 lines
46 KiB
HTML
<!--
|
|
Photo Gallery Template, shared by Site Photos + User Photos.
|
|
|
|
When Site Gallery: .IsSiteGallery is defined and true.
|
|
When User Gallery: .User is defined, .IsOwnPhotos may be.
|
|
-->
|
|
{{define "title"}}
|
|
{{if .IsSiteGallery}}
|
|
Member Gallery
|
|
{{else}}
|
|
Photos of {{.User.Username}}
|
|
{{if eq .User.Visibility "private"}}<sup class="fa fa-mask ml-2 is-size-6" title="Private Profile"></sup>{{end}}
|
|
{{end}}
|
|
{{end}}
|
|
|
|
<!-- Reusable card body -->
|
|
{{define "card-body"}}
|
|
<div>
|
|
<small class="has-text-grey">Uploaded {{.CreatedAt.Format "Jan _2 2006 15:04:05"}}</small>
|
|
{{if .Views}}
|
|
<small class="has-text-grey is-size-7 ml-2">
|
|
<i class="fa fa-eye"></i>
|
|
{{.Views}}
|
|
</small>
|
|
{{end}}
|
|
</div>
|
|
<div class="mt-2">
|
|
{{if .Pinned}}
|
|
<span class="tag is-success is-light">
|
|
<span class="icon"><i class="fa fa-thumbtack"></i></span>
|
|
<span>Pinned</span>
|
|
</span>
|
|
{{end}}
|
|
|
|
{{if .Explicit}}
|
|
<span class="tag is-danger is-light">
|
|
<span class="icon"><i class="fa fa-fire"></i></span>
|
|
<span>Explicit</span>
|
|
</span>
|
|
{{end}}
|
|
|
|
{{if eq .Visibility "public"}}
|
|
<span class="tag is-info is-light">
|
|
<span class="icon"><i class="fa fa-eye"></i></span>
|
|
<span>
|
|
Public
|
|
</span>
|
|
</span>
|
|
{{else if eq .Visibility "friends"}}
|
|
<span class="tag is-warning is-light">
|
|
<span class="icon"><i class="fa fa-user-group"></i></span>
|
|
<span>
|
|
Friends
|
|
</span>
|
|
</span>
|
|
{{else}}
|
|
<span class="tag is-private is-light">
|
|
<span class="icon"><i class="fa fa-lock"></i></span>
|
|
<span>
|
|
Private
|
|
</span>
|
|
</span>
|
|
{{end}}
|
|
|
|
{{if .Gallery}}
|
|
<span class="tag is-success is-light">
|
|
<span class="icon"><i class="fa fa-image"></i></span>
|
|
<span>Gallery</span>
|
|
</span>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
|
|
<!-- Reusable card footer -->
|
|
{{define "card-footer"}}
|
|
<label class="card-footer-item checkbox">
|
|
<input type="checkbox" class="nonshy-edit-photo-id"
|
|
name="id"
|
|
value="{{.ID}}">
|
|
</label>
|
|
<a class="card-footer-item" href="/photo/edit?id={{.ID}}">
|
|
<span class="icon"><i class="fa fa-edit"></i></span>
|
|
<span>Edit</span>
|
|
</a>
|
|
<a class="card-footer-item has-text-danger" href="/photo/delete?id={{.ID}}">
|
|
<span class="icon"><i class="fa fa-trash"></i></span>
|
|
<span>Delete</span>
|
|
</a>
|
|
{{end}}
|
|
|
|
<!-- Main content template -->
|
|
{{define "content"}}
|
|
{{if not .IsSiteGallery}}
|
|
<style type="text/css">
|
|
{{template "profile-theme-hero-style" .User}}
|
|
</style>
|
|
{{end}}
|
|
<div class="container">
|
|
<section class="hero is-link is-bold">
|
|
<div class="hero-body">
|
|
<div class="container">
|
|
<div class="level">
|
|
<div class="level-left">
|
|
<h1 class="title">
|
|
<span class="icon mr-4"><i class="fa fa-image"></i></span>
|
|
<span>{{template "title" .}}</span>
|
|
</h1>
|
|
</div>
|
|
{{if or .IsOwnPhotos .IsSiteGallery}}
|
|
<div class="level-right">
|
|
<div>
|
|
<a href="/photo/upload" class="button">
|
|
<span class="icon"><i class="fa fa-upload"></i></span>
|
|
<span>Upload Photo</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ugly hack.. needed by the card-footers later below. -->
|
|
{{$Root := .}}
|
|
|
|
<div class="block p-4">
|
|
<!-- Profile Tab for user view -->
|
|
{{if not .IsSiteGallery}}
|
|
<div class="tabs is-boxed">
|
|
<ul>
|
|
<li>
|
|
<a href="/u/{{.User.Username}}">
|
|
<span class="icon is-small">
|
|
<i class="fa fa-user"></i>
|
|
</span>
|
|
<span>Profile</span>
|
|
</a>
|
|
</li>
|
|
<li class="is-active">
|
|
<a>
|
|
<span class="icon is-small">
|
|
<i class="fa fa-image"></i>
|
|
</span>
|
|
<span>
|
|
Photos
|
|
{{if .PhotoCount}}<span class="tag is-link is-light ml-1">{{.PhotoCount}}</span>{{end}}
|
|
</span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="/u/{{.User.Username}}/notes">
|
|
<span class="icon is-small">
|
|
<i class="fa fa-pen-to-square"></i>
|
|
</span>
|
|
<span>
|
|
Notes
|
|
{{if .NoteCount}}<span class="tag is-link is-light ml-1">{{.NoteCount}}</span>{{end}}
|
|
</span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="/u/{{.User.Username}}/friends">
|
|
<span class="icon is-small">
|
|
<i class="fa fa-user-group"></i>
|
|
</span>
|
|
<span>
|
|
Friends
|
|
{{if .FriendCount}}<span class="tag is-link is-light ml-1">{{.FriendCount}}</span>{{end}}
|
|
</span>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
{{end}}
|
|
|
|
<!-- Photo Detail Modal -->
|
|
<div class="modal" id="detail-modal">
|
|
<div class="modal-background"></div>
|
|
<div class="modal-content photo-modal">
|
|
<!-- Notes: to get the image to always scale and fit on screen, it is made as a background image in CSS
|
|
on the detailImg div; but we don't have the image's minimum size here, so the hidden <img> inside
|
|
provides the size pushing to make it visible on screen, otherwise the divs are 0x0 pixels and nothing
|
|
would be visible. -->
|
|
<div id="detailImg">
|
|
<img style="visibility: hidden">
|
|
|
|
<!-- Alt Text button for accessibility -->
|
|
<button class="button is-small alt-text py-1 px-2">
|
|
<strong>ALT</strong>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<button class="modal-close is-large" aria-label="close"></button>
|
|
</div>
|
|
|
|
<!-- Shy User alert banner (Site Gallery) -->
|
|
{{if and .IsSiteGallery .IsShyUser}}
|
|
<div class="notification is-danger is-light">
|
|
<i class="fa fa-exclamation-triangle"></i> You have a <strong>Shy Account</strong> so you will only see
|
|
pictures of you and your friends here. <a href="/faq#shy-faqs">Learn more <small class="fa fa-external-link"></small></a>
|
|
</div>
|
|
{{end}}
|
|
|
|
<!-- Shy User alert banner (User Gallery - IsShyFrom) -->
|
|
{{if .IsShyFrom}}
|
|
<div class="notification is-danger is-light">
|
|
<i class="fa fa-exclamation-triangle"></i> You have a <strong>Shy Account</strong> and you are not friends
|
|
with this person so can not see their gallery. <a href="/faq#shy-faqs">Learn more <small class="fa fa-external-link"></small></a>
|
|
</div>
|
|
{{end}}
|
|
|
|
<!-- Notice if the current user can not see the user's default profile picture -->
|
|
{{if .ProfilePictureHiddenVisibility}}
|
|
<div class="block">
|
|
<i class="fa fa-info-circle mr-1"></i>
|
|
<strong>Notice:</strong>
|
|
@{{.User.Username}}'s default profile picture is set to
|
|
{{if eq .ProfilePictureHiddenVisibility "friends"}}
|
|
<img src="/static/img/shy-friends.png" width="16" height="16">
|
|
<strong class="has-text-warning">Friends only</strong>
|
|
{{else}}
|
|
<img src="/static/img/shy-private.png" width="16" height="16">
|
|
<strong class="has-text-private">Private</strong>
|
|
{{end}}
|
|
visibility and can not be seen by you.
|
|
<a href="/faq#private-avatar" target="_blank">Learn more <i class="fa fa-external-link"></i></a>
|
|
</div>
|
|
{{end}}
|
|
|
|
<div class="block">
|
|
<div class="level mb-2">
|
|
<div class="level-left">
|
|
<div class="level-item">
|
|
{{if .Pager.Total}}
|
|
<span>
|
|
Found <strong>{{FormatNumberCommas .Pager.Total}}</strong> photo{{Pluralize64 .Pager.Total}} (page {{.Pager.Page}} of {{.Pager.Pages}}).
|
|
{{if .ExplicitCount}}
|
|
{{.ExplicitCount}} explicit photo{{Pluralize64 .ExplicitCount}} hidden per your <a href="/settings#prefs">settings</a>.
|
|
{{end}}
|
|
</span>
|
|
{{else if .ExplicitCount}}
|
|
<!-- No pager, but still show explicit hint, e.g. in case user filters by Private but all privates are explicit -->
|
|
<span>
|
|
{{.ExplicitCount}} explicit photo{{Pluralize64 .ExplicitCount}} hidden per your <a href="/settings#prefs">settings</a>.
|
|
</span>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="level-right">
|
|
<div class="level-item">
|
|
<div class="tabs is-toggle is-small is-hidden-mobile">
|
|
<ul>
|
|
<li{{if eq .ViewStyle "cards"}} class="is-active"{{end}}>
|
|
<a href="{{.Request.URL.Path}}?{{QueryPlus "view" "cards"}}">Cards</a>
|
|
</li>
|
|
<li{{if eq .ViewStyle "full"}} class="is-active"{{end}}>
|
|
<a href="{{.Request.URL.Path}}?{{QueryPlus "view" "full"}}">Full</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Show an "Unsubscribe to this user's new photo notifications" if you are Friends. -->
|
|
{{if .AreFriends}}
|
|
<p class="block">
|
|
<a href="/comments/subscription?table_name=friend.photos&table_id={{.User.Username}}&next={{UrlEncode .Request.URL.String}}&subscribe={{if .AreNotificationsMuted}}true{{else}}false{{end}}"
|
|
class="{{if .AreNotificationsMuted}}has-text-success{{else}}{{end}}">
|
|
<span class="icon"><i class="fa fa-bell{{if not .AreNotificationsMuted}}-slash{{end}}"></i></span>
|
|
<span>
|
|
{{if .AreNotificationsMuted}}
|
|
Enable notifications about <strong>{{.User.Username}}</strong>'s new photos
|
|
{{else}}
|
|
Mute notifications about <strong>{{.User.Username}}</strong>'s new photos
|
|
{{end}}
|
|
</span>
|
|
</a>
|
|
</p>
|
|
{{end}}
|
|
|
|
<!-- If viewing our own profile, and we don't have a profile picture set, offer advice. -->
|
|
{{if and (not .IsSiteGallery) (eq .CurrentUser.ProfilePhoto.ID 0) (eq .CurrentUser.ID .User.ID)}}
|
|
<div class="notification is-success is-light content">
|
|
<p>
|
|
<i class="fa-regular fa-id-badge mr-1"></i>
|
|
<strong>Your default profile picture is not set</strong>
|
|
|
|
<p>
|
|
Your default profile picture is currently not set to anything, and appears to other members as
|
|
the default blue <img src="/static/img/shy.png" width="16" height="16"> placeholder image.
|
|
</p>
|
|
|
|
<ul>
|
|
<li>
|
|
To upload a <strong>new</strong> profile picture, <a href="/photo/upload?intent=profile_pic">click here</a>.
|
|
</li>
|
|
<li>
|
|
To use one of your <strong>existing</strong> photos as your profile picture:
|
|
<ol class="my-2">
|
|
<li>
|
|
Click on the "Edit" button beneath one of your photos below.
|
|
</li>
|
|
<li>
|
|
On the edit page, below the picture, click on the button to "Set this as my profile photo (crop image)"
|
|
and select the square shape you want for your profile pic.
|
|
</li>
|
|
<li>
|
|
Click on "Save Changes" when done!
|
|
</li>
|
|
</ol>
|
|
</li>
|
|
</ul>
|
|
|
|
<p>
|
|
Having a profile picture set, along with an approved <a href="/photo/certification">certification photo</a>,
|
|
is required to access the social features on {{PrettyTitle}} such as the chat room, forums and member directory.
|
|
</p>
|
|
</p>
|
|
</div>
|
|
{{end}}
|
|
|
|
<!-- Indicator if friends-only is selected. -->
|
|
{{if eq .FilterWho "friends"}}
|
|
<div class="notification is-success is-light">
|
|
Showing you all recent photos from <strong>yourself & your friends.</strong>
|
|
<a href="{{.Request.URL.Path}}?who=everybody">See all certified members' gallery photos?</a>
|
|
</div>
|
|
{{else if eq .FilterWho "friends+private"}}
|
|
<div class="notification is-success is-light">
|
|
Showing you all recent photos from <strong>yourself & your friends</strong> as well
|
|
as any <strong>private photos shared with you</strong> by others on the site (if they are
|
|
marked to appear in the Site Gallery).
|
|
</div>
|
|
{{else if eq .FilterWho "likes"}}
|
|
<div class="notification is-success is-light">
|
|
Showing you photos that you have <i class="fa fa-heart"></i> <strong>Liked.</strong>
|
|
</div>
|
|
{{end}}
|
|
|
|
<!-- Filters -->
|
|
<div class="block">
|
|
<form action="{{.Request.URL.Path}}" method="GET">
|
|
|
|
<div class="card nonshy-collapsible-mobile">
|
|
<header class="card-header has-background-link-light">
|
|
<p class="card-header-title has-text-dark">
|
|
Search Filters
|
|
</p>
|
|
<button class="card-header-icon" type="button">
|
|
<span class="icon">
|
|
<i class="fa fa-angle-up"></i>
|
|
</span>
|
|
</button>
|
|
</header>
|
|
<div class="card-content">
|
|
<div class="columns is-multiline mb-0">
|
|
|
|
<!-- Site Gallery: friends-only filter -->
|
|
{{if .IsSiteGallery}}
|
|
<div class="column">
|
|
<div class="field">
|
|
<label class="label" for="who">Whose photos:</label>
|
|
<div class="select is-fullwidth">
|
|
<select id="who" name="who">
|
|
<option value="friends"{{if eq .FilterWho "friends"}} selected{{end}}>Myself & friends only</option>
|
|
<option value="friends+private"{{if eq .FilterWho "friends+private"}} selected{{end}}>Myself, friends, & private photo grants</option>
|
|
<option value="likes"{{if eq .FilterWho "likes"}} selected{{end}}>Photos I have 'liked'</option>
|
|
<option value="everybody"{{if eq .FilterWho "everybody"}} selected{{end}}>All certified members</option>
|
|
{{if .CurrentUser.HasAdminScope "social.moderator.photo"}}
|
|
<option value="uncertified"{{if eq .FilterWho "uncertified"}} selected{{end}}>☮ Non-certified members</option>
|
|
{{end}}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if or .CurrentUser.Explicit .IsOwnPhotos}}
|
|
<div class="column">
|
|
<div class="field">
|
|
<label class="label" for="explicit">Explicit:</label>
|
|
<div class="select is-fullwidth">
|
|
<select id="explicit" name="explicit">
|
|
<option value="">Show all</option>
|
|
<option value="true"{{if eq .FilterExplicit "true"}} selected{{end}}>Only explicit</option>
|
|
<option value="false"{{if eq .FilterExplicit "false"}} selected{{end}}>Hide explicit</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
<div class="column">
|
|
<div class="field">
|
|
<label class="label" for="visibility">Visibility:</label>
|
|
<div class="select is-fullwidth">
|
|
<select id="visibility" name="visibility">
|
|
<option value="">All photos</option>
|
|
<option value="public"{{if eq .FilterVisibility "public"}} selected{{end}}>Public only</option>
|
|
|
|
<!-- Friends & Private: always show on Site Gallery, show if available on User Gallery -->
|
|
{{if or .IsSiteGallery .AreFriends .IsOwnPhotos}}
|
|
<option value="friends"{{if eq .FilterVisibility "friends"}} selected{{end}}>Friends only</option>
|
|
{{end}}
|
|
{{if or .IsSiteGallery .AreWeGrantedPrivate .IsOwnPhotos}}
|
|
<option value="private"{{if eq .FilterVisibility "private"}} selected{{end}}>Private only</option>
|
|
{{end}}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="column">
|
|
<div class="field">
|
|
<label class="label" for="sort">Sort by:</label>
|
|
<div class="select is-fullwidth">
|
|
<select id="sort" name="sort">
|
|
{{if not .IsSiteGallery}}
|
|
<option value="pinned desc nulls last, updated_at desc"{{if eq .Sort "pinned desc nulls last, updated_at desc"}} selected{{end}}>
|
|
Pinned, recently updated
|
|
</option>
|
|
{{end}}
|
|
<option value="created_at desc"{{if eq .Sort "created_at desc"}} selected{{end}}>Most recent</option>
|
|
<option value="created_at asc"{{if eq .Sort "created_at asc"}} selected{{end}}>Oldest first</option>
|
|
<option value="like_count desc"{{if eq .Sort "like_count desc"}} selected{{end}}>Most likes</option>
|
|
<option value="comment_count desc"{{if eq .Sort "comment_count desc"}} selected{{end}}>Most comments</option>
|
|
<option value="views desc"{{if eq .Sort "views desc"}} selected{{end}}>Most views</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{if and .IsSiteGallery (.CurrentUser.HasAdminScope "social.moderator.photo")}}
|
|
<div class="column">
|
|
<div class="field">
|
|
<label class="label has-text-danger" for="admin_view">Admin view:</label>
|
|
<div class="select is-fullwidth">
|
|
<select id="admin_view" name="admin_view">
|
|
<option value="">Default (disabled)</option>
|
|
<option value="true"{{if .AdminView}} selected{{end}}>Show all photos</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
</div>
|
|
<div class="has-text-centered">
|
|
<a href="{{.Request.URL.Path}}" class="button">Reset</a>
|
|
<button type="submit" class="button is-success">
|
|
Apply Filters
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Retain cards vs. full parameter -->
|
|
<input type="hidden" name="view" value="{{.ViewStyle}}">
|
|
|
|
</form>
|
|
</div>
|
|
|
|
{{if .IsOwnPhotos}}
|
|
<div class="block">
|
|
<a href="/photo/private" class="has-text-private">
|
|
<span class="icon"><i class="fa fa-lock"></i></span>
|
|
<span>Manage who can see <strong>my</strong> private photos</span>
|
|
</a>
|
|
</div>
|
|
{{else if not .IsSiteGallery}}
|
|
<div class="block">
|
|
{{if not .IsMyPrivateUnlockedFor}}
|
|
<a href="/photo/private/share?to={{.User.Username}}" class="has-text-private">
|
|
<span class="icon"><i class="fa fa-unlock"></i></span>
|
|
<span>Grant <strong>{{.User.Username}}</strong> access to see <strong>my</strong> private photos</span>
|
|
</a>
|
|
{{else}}
|
|
<span class="icon"><i class="fa fa-unlock has-text-private"></i></span>
|
|
<span>You had granted <strong>{{.User.Username}}</strong> access to see <strong>your</strong> private photos.</span>
|
|
<a href="/photo/private">Manage that here.</a>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if .AreWeGrantedPrivate}}
|
|
<div class="block mt-0">
|
|
<span class="icon"><i class="fa fa-eye has-text-private"></i></span>
|
|
<strong>{{.User.Username}}</strong> has <span class="has-text-private">granted</span> you
|
|
access to see their private photos.
|
|
</div>
|
|
{{end}}
|
|
|
|
{{SimplePager .Pager}}
|
|
|
|
<!-- Form to wrap the gallery, e.g. for batch edits on user views. -->
|
|
<form action="/photo/batch-edit">
|
|
|
|
<!-- "Full" view style? (blog style) -->
|
|
{{if eq .ViewStyle "full"}}
|
|
{{range .Photos}}
|
|
<div class="card block">
|
|
<header class="card-header {{if .Explicit}}has-background-danger{{else}}has-background-link{{end}}">
|
|
<!-- Site Gallery header -->
|
|
{{if $Root.IsSiteGallery}}
|
|
<div class="card-header-title has-text-light">
|
|
{{if $Root.UserMap.Has .UserID}}
|
|
{{$Owner := $Root.UserMap.Get .UserID}}
|
|
<div class="columns is-mobile is-gapless nonshy-fullwidth">
|
|
<div class="column is-narrow mr-2">
|
|
{{template "avatar-24x24" $Owner}}
|
|
</div>
|
|
<div class="column">
|
|
<a href="/u/{{$Owner.Username}}" class="has-text-light">
|
|
{{$Owner.Username}}
|
|
<i class="fa fa-external-link ml-2"></i>
|
|
</a>
|
|
</div>
|
|
<div class="column is-narrow">
|
|
<span class="icon">
|
|
{{if eq .Visibility "friends"}}
|
|
<i class="fa fa-user-group has-text-warning" title="Friends"></i>
|
|
{{else if eq .Visibility "private"}}
|
|
<i class="fa fa-lock has-text-private-light" title="Private"></i>
|
|
{{else}}
|
|
<i class="fa fa-eye has-text-link-light" title="Public"></i>
|
|
{{end}}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
{{else}}
|
|
<span class="fa fa-user mr-2"></span>
|
|
<span>[deleted]</span>
|
|
{{end}}
|
|
</div>
|
|
{{else}}
|
|
<!-- User Gallery Full Header -->
|
|
<p class="card-header-title has-text-light">
|
|
<span class="icon">
|
|
<i class="fa fa-image"></i>
|
|
</span>
|
|
{{or .Caption "Photo"}}
|
|
</p>
|
|
{{end}}
|
|
</header>
|
|
|
|
<div class="card-image has-text-centered is-clipped">
|
|
<!-- GIF video? -->
|
|
{{if HasSuffix .Filename ".mp4"}}
|
|
<video loop controls controlsList="nodownload" playsinline
|
|
class="js-modal-trigger"
|
|
data-url="{{PhotoURL .Filename}}" data-photo-id="{{.ID}}"
|
|
{{if .AltText}}title="{{.AltText}}"{{end}}
|
|
{{if BlurExplicit .}}class="blurred-explicit"
|
|
{{else if (not (eq ($Root.CurrentUser.GetProfileField "autoplay_gif") "false"))}}autoplay
|
|
{{end}}>
|
|
<source src="{{PhotoURL .Filename}}" type="video/mp4">
|
|
</video>
|
|
{{else}}
|
|
<a href="/photo/view?id={{.ID}}" data-url="{{PhotoURL .Filename}}" data-photo-id="{{.ID}}" target="_blank"
|
|
class="js-modal-trigger">
|
|
<img src="{{PhotoURL .Filename}}" loading="lazy"
|
|
{{if BlurExplicit .}}class="blurred-explicit"{{end}}
|
|
{{if .AltText}}alt="{{.AltText}}" title="{{.AltText}}"{{end}}>
|
|
</a>
|
|
{{end}}
|
|
</div>
|
|
|
|
<div class="card-content">
|
|
{{if .Caption}}
|
|
{{.Caption}}
|
|
{{else}}<em>No caption</em>{{end}}
|
|
|
|
{{template "card-body" .}}
|
|
|
|
<!-- Quick mark photo as explicit -->
|
|
{{if and (not .Explicit) (ne .UserID $Root.CurrentUser.ID) (not .HasAdminLabelNonExplicit)}}
|
|
<div class="mt-2">
|
|
<a href="#"
|
|
class="has-text-danger is-size-7 nonshy-mark-explicit"
|
|
data-photo-id="{{.ID}}" data-photo-url="{{PhotoURL .Filename}}">
|
|
<i class="fa fa-fire mr-1"></i>
|
|
Should this photo be marked Explicit?
|
|
</a>
|
|
</div>
|
|
{{end}}
|
|
|
|
<!-- Like & Comments buttons -->
|
|
{{if not $Root.AdminView}}
|
|
<div class="mt-4 columns is-centered is-mobile is-gapless">
|
|
<div class="column is-narrow mr-1">
|
|
{{$Like := $Root.LikeMap.Get .ID}}
|
|
<button type="button" class="button is-small nonshy-like-button"
|
|
data-table-name="photos" data-table-id="{{.ID}}"
|
|
title="Like">
|
|
<span class="icon{{if $Like.UserLikes}} has-text-danger{{end}}"><i class="fa fa-heart"></i></span>
|
|
<span class="nonshy-likes">
|
|
Like
|
|
{{if gt $Like.Count 0}}
|
|
({{$Like.Count}})
|
|
{{end}}
|
|
</span>
|
|
</button>
|
|
</div>
|
|
<div class="column is-narrow">
|
|
{{$Comments := $Root.CommentMap.Get .ID}}
|
|
<a href="/photo/view?id={{.ID}}#comments" class="button is-small">
|
|
<span class="icon"><i class="fa fa-comment"></i></span>
|
|
<span>{{$Comments}} Comment{{Pluralize64 $Comments}}</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
|
|
<footer class="card-footer">
|
|
{{if or $Root.IsOwnPhotos ($Root.CurrentUser.HasAdminScope "social.moderator.photo")}}
|
|
{{template "card-footer" .}}
|
|
{{end}}
|
|
|
|
{{if not $Root.IsOwnPhotos}}
|
|
<a class="card-footer-item has-text-danger" href="/contact?intent=report&subject=report.photo&id={{.ID}}">
|
|
<span class="icon"><i class="fa fa-flag"></i></span>
|
|
<span>Report</span>
|
|
</a>
|
|
{{end}}
|
|
</footer>
|
|
|
|
</div>
|
|
{{end}}
|
|
|
|
<!-- "Cards" style (default) -->
|
|
{{else}}
|
|
<div class="columns is-multiline">
|
|
{{range .Photos}}
|
|
<div class="column is-one-quarter-desktop is-half-tablet">
|
|
<div class="card">
|
|
<!-- Header only on Site Gallery version -->
|
|
{{if $Root.IsSiteGallery}}
|
|
<header class="card-header {{if .Explicit}}has-background-danger{{else}}has-background-link{{end}}">
|
|
<div class="card-header-title has-text-light">
|
|
{{if $Root.UserMap.Has .UserID}}
|
|
{{$Owner := $Root.UserMap.Get .UserID}}
|
|
<div class="columns is-mobile is-gapless nonshy-fullwidth">
|
|
<div class="column is-narrow mr-2">
|
|
{{template "avatar-24x24" $Owner}}
|
|
</div>
|
|
<div class="column">
|
|
<a href="/u/{{$Owner.Username}}" class="has-text-light">
|
|
{{$Owner.Username}}
|
|
<i class="fa fa-external-link ml-2"></i>
|
|
</a>
|
|
</div>
|
|
<div class="column is-narrow">
|
|
<span class="icon">
|
|
{{if eq .Visibility "friends"}}
|
|
<i class="fa fa-user-group has-text-warning" title="Friends"></i>
|
|
{{else if eq .Visibility "private"}}
|
|
<i class="fa fa-lock has-text-private-light" title="Private"></i>
|
|
{{else}}
|
|
<i class="fa fa-eye has-text-link-light" title="Public"></i>
|
|
{{end}}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
{{else}}
|
|
<span class="fa fa-user mr-2"></span>
|
|
<span>[deleted]</span>
|
|
{{end}}
|
|
</div>
|
|
</header>
|
|
{{end}}
|
|
|
|
<div class="card-image has-text-centered is-clipped">
|
|
<!-- GIF video? -->
|
|
{{if HasSuffix .Filename ".mp4"}}
|
|
<video loop controls controlsList="nodownload" playsinline
|
|
class="js-modal-trigger"
|
|
data-url="{{PhotoURL .Filename}}" data-photo-id="{{.ID}}"
|
|
{{if .AltText}}title="{{.AltText}}"{{end}}
|
|
{{if BlurExplicit .}}class="blurred-explicit"
|
|
{{else if (not (eq ($Root.CurrentUser.GetProfileField "autoplay_gif") "false"))}}autoplay
|
|
{{end}}>
|
|
<source src="{{PhotoURL .Filename}}" type="video/mp4">
|
|
</video>
|
|
{{else}}
|
|
<a href="/photo/view?id={{.ID}}" data-url="{{PhotoURL .Filename}}" data-photo-id="{{.ID}}" target="_blank"
|
|
class="js-modal-trigger">
|
|
<img src="{{PhotoURL .Filename}}" loading="lazy"
|
|
{{if BlurExplicit .}}class="blurred-explicit"{{end}}
|
|
{{if .AltText}}alt="{{.AltText}}" title="{{.AltText}}"{{end}}>
|
|
</a>
|
|
{{end}}
|
|
</div>
|
|
<div class="card-content">
|
|
{{if .Caption}}
|
|
{{.Caption}}
|
|
{{else}}<em>No caption</em>{{end}}
|
|
|
|
{{template "card-body" .}}
|
|
|
|
<!-- Quick mark photo as explicit -->
|
|
{{if and (not .Explicit) (ne .UserID $Root.CurrentUser.ID) (not .HasAdminLabelNonExplicit)}}
|
|
<div class="mt-2">
|
|
<a href="#"
|
|
class="has-text-danger is-size-7 nonshy-mark-explicit"
|
|
data-photo-id="{{.ID}}" data-photo-url="{{PhotoURL .Filename}}">
|
|
<i class="fa fa-fire mr-1"></i>
|
|
Should this photo be marked Explicit?
|
|
</a>
|
|
</div>
|
|
{{end}}
|
|
|
|
<!-- Like & Comments buttons -->
|
|
{{if not $Root.AdminView}}
|
|
<div class="mt-4 columns is-centered is-mobile is-gapless">
|
|
<div class="column is-narrow mr-1">
|
|
{{$Like := $Root.LikeMap.Get .ID}}
|
|
<button type="button" class="button is-small nonshy-like-button"
|
|
data-table-name="photos" data-table-id="{{.ID}}"
|
|
title="Like">
|
|
<span class="icon{{if $Like.UserLikes}} has-text-danger{{end}}"><i class="fa fa-heart"></i></span>
|
|
<span class="nonshy-likes">
|
|
Like
|
|
{{if gt $Like.Count 0}}
|
|
({{$Like.Count}})
|
|
{{end}}
|
|
</span>
|
|
</button>
|
|
</div>
|
|
<div class="column is-narrow">
|
|
{{$Comments := $Root.CommentMap.Get .ID}}
|
|
<a href="/photo/view?id={{.ID}}#comments" class="button is-small">
|
|
<span class="icon"><i class="fa fa-comment"></i></span>
|
|
<span>{{$Comments}} Comment{{Pluralize64 $Comments}}</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
|
|
<footer class="card-footer">
|
|
{{if or $Root.IsOwnPhotos ($Root.CurrentUser.HasAdminScope "social.moderator.photo")}}
|
|
{{template "card-footer" .}}
|
|
{{end}}
|
|
|
|
{{if not $Root.IsOwnPhotos}}
|
|
<a class="card-footer-item has-text-danger" href="/contact?intent=report&subject=report.photo&id={{.ID}}">
|
|
<span class="icon"><i class="fa fa-flag"></i></span>
|
|
<span class="is-hidden-desktop">Report</span>
|
|
</a>
|
|
{{end}}
|
|
</footer>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
{{end}}<!-- ViewStyle -->
|
|
|
|
{{SimplePager .Pager}}
|
|
|
|
<!-- Bulk user actions to their photos -->
|
|
{{if or .IsOwnPhotos (.CurrentUser.HasAdminScope "social.moderator.photo")}}
|
|
<hr>
|
|
<div class="columns is-multiline is-mobile my-4">
|
|
<div class="column is-narrow">
|
|
<div class="buttons has-addons">
|
|
<button type="button" class="button" id="nonshy-select-all">
|
|
<i class="fa fa-square-check"></i>
|
|
</button>
|
|
<button type="button" class="button" id="nonshy-select-none">
|
|
<i class="fa fa-square"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="column" id="nonshy-edit-buttons">
|
|
<button type="submit" class="button is-small is-danger is-outlined"
|
|
name="intent"
|
|
value="delete">
|
|
<i class="fa fa-trash mr-2"></i>
|
|
Delete
|
|
</button>
|
|
|
|
<button type="submit" class="button mx-1 is-small is-info is-outlined"
|
|
name="intent"
|
|
value="visibility">
|
|
<i class="fa fa-eye mr-2"></i>
|
|
Edit Visibility
|
|
</button>
|
|
|
|
<span id="nonshy-count-selected" class="is-size-7 ml-2"></span>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</form><!-- end gallery form for batch edits -->
|
|
</div>
|
|
{{end}}
|
|
|
|
<!-- Admin change log link -->
|
|
{{if .CurrentUser.HasAdminScope "admin.changelog"}}
|
|
<div class="block">
|
|
<a href="/admin/changelog?table_name=photos{{if .User}}&about_user_id={{.User.ID}}{{end}}" class="button is-small has-text-warning">
|
|
<span class="icon"><i class="fa fa-peace mr-1"></i></span>
|
|
<span>{{if .User}}User{{else}}Site{{end}} Gallery change log</span>
|
|
</a>
|
|
</div>
|
|
{{end}}
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<script type="text/javascript">
|
|
|
|
{{if or .IsOwnPhotos (.CurrentUser.HasAdminScope "social.moderator.photo")}}
|
|
// Batch edit controls
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
const checkboxes = document.getElementsByClassName("nonshy-edit-photo-id"),
|
|
$checkAll = document.querySelector("#nonshy-select-all"),
|
|
$checkNone = document.querySelector("#nonshy-select-none"),
|
|
$countSelected = document.querySelector("#nonshy-count-selected"),
|
|
$submitButtons = document.querySelector("#nonshy-edit-buttons");
|
|
|
|
$submitButtons.style.display = "none";
|
|
|
|
const setAllChecked = (v) => {
|
|
for (let box of checkboxes) {
|
|
box.checked = v;
|
|
}
|
|
};
|
|
|
|
const areAnyChecked = () => {
|
|
let any = false,
|
|
count = 0;
|
|
for (let box of checkboxes) {
|
|
if (box.checked) {
|
|
any = true;
|
|
count++;
|
|
}
|
|
}
|
|
|
|
// update the selected count
|
|
$countSelected.innerHTML = count > 0 ? `${count} selected.` : "";
|
|
$countSelected.style.display = count > 0 ? "" : "none";
|
|
return any;
|
|
};
|
|
|
|
const showHideButtons = () => {
|
|
$submitButtons.style.display = areAnyChecked() ? "" : "none";
|
|
};
|
|
showHideButtons();
|
|
|
|
// Check/Uncheck All buttons.
|
|
$checkAll.addEventListener("click", (e) => {
|
|
setAllChecked(true);
|
|
showHideButtons();
|
|
});
|
|
$checkNone.addEventListener("click", (e) => {
|
|
setAllChecked(false);
|
|
showHideButtons();
|
|
});
|
|
|
|
// When checkboxes are toggled.
|
|
for (let box of checkboxes) {
|
|
box.addEventListener("change", (e) => {
|
|
showHideButtons();
|
|
});
|
|
}
|
|
});
|
|
{{end}}
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
// Get our modal to trigger it on click of a detail img.
|
|
let $modal = document.querySelector("#detail-modal"),
|
|
$altText = $modal.getElementsByTagName("button")[0];
|
|
|
|
function setModalImage(url, altText) {
|
|
let $modalImg = document.querySelector("#detailImg"),
|
|
$img = $modalImg.getElementsByTagName("img")[0];
|
|
$img.src = url;
|
|
$modalImg.style.backgroundImage = `url(${url})`;
|
|
|
|
// Alt text?
|
|
$modalImg.title = altText;
|
|
$altText.style.display = altText ? "block" : "none";
|
|
$altText.onclick = (e) => {
|
|
window.alert(altText);
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function markImageViewed(photoID) {
|
|
fetch(`/v1/photo/${photoID}/view`, {
|
|
method: "POST",
|
|
mode: "same-origin",
|
|
cache: "no-cache",
|
|
credentials: "same-origin",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
}).then(response => response.json())
|
|
.then(data => {
|
|
if (data.StatusCode !== 200) {
|
|
console.error("When marking photo %d as viewed: status code %d: %s", photoID, data.StatusCode, data.data.error);
|
|
return;
|
|
}
|
|
}).catch(window.alert);
|
|
}
|
|
|
|
document.querySelectorAll(".js-modal-trigger").forEach(node => {
|
|
let $img = node.getElementsByTagName("img"),
|
|
$video = node.tagName === 'VIDEO' ? node : null,
|
|
photoID = node.dataset.photoId,
|
|
altText = $img[0] != undefined ? $img[0].alt : '';
|
|
|
|
// Video (animated GIF) handlers.
|
|
if ($video !== null) {
|
|
|
|
// Log this video viewed if the user interacts with it in any way.
|
|
// Note: because videos don't open in the lightbox modal.
|
|
['pause', 'mouseover'].forEach(event => {
|
|
$video.addEventListener(event, (e) => {
|
|
// Log a view of this video.
|
|
markImageViewed(photoID);
|
|
});
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
// Images: open in the lightbox modal.
|
|
node.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
setModalImage(node.dataset.url, altText);
|
|
$modal.classList.add("is-active");
|
|
|
|
// Log a view of this photo.
|
|
markImageViewed(photoID);
|
|
})
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<!-- Mark Explicit modal -->
|
|
{{template "mark-explicit-modal" .}}
|
|
{{end}}
|