/* * We have a few competing goals: * * First, be as responsive as possible. Preload nearby post HTML and their images. * * If data in a post page changes, eg. if the user votes, then coming back to the page * later should retain the changes. This means either requesting the page again, or * retaining the document node and reusing it, so we preserve the changes that were * made in-place. * * Don't use too much memory. If we keep every document node in memory as we use it, * the images will probably be kept around too. Release older nodes, so the browser * is more likely to release images that havn't been used in a while. * * We do the following: * - When we load a new post, it's formatted and its scripts are evaluated normally. * - When we're replacing the displayed post, its node is stashed away in a node cache. * - If we come back to the post while it's in the node cache, we'll use the node directly. * - HTML and images for posts are preloaded. We don't use a simple mechanism like * Preload.preload_raw, because Opera's caching is broken for XHR and it'll always * do a slow revalidation. * - We don't depend on browser caching for HTML. That would require us to expire a * page when we switch away from it if we've made any changes (eg. voting), so we * don't pull an out-of-date page next time. This is slower, and would require us * to be careful about expiring the cache. */ BrowserView = function(container) { this.container = container; /* The post that we currently want to display. This will be either one of the * current html_preloads, or be the displayed_post_id. */ this.wanted_post_id = null; this.wanted_post_frame = null; /* The post that's currently actually being displayed. */ this.displayed_post_id = null; this.displayed_post_frame = null; this.current_ajax_request = null; this.last_preload_request = []; this.last_preload_request_active = false; this.image_pool = new ImgPoolHandler(); this.img_box = this.container.down(".image-box"); this.container.down(".image-canvas"); /* Pop. Show large by default { */ (function($, B){ /* MI: If mobile browser, do nothing. */ if (Prototype.BrowserIsMobile) return; var COOKIE_NAME = "browse_large_by_default"; B.large_by_default = jQuery.cookie(COOKIE_NAME) === "0" ? false : true; var li = $($('.post-info-right-edge li')[0]).css("position", "relative"); var div = $('.default_to_large_cont'), a = div.children('a'), s = a.children('span'); function toggle_show_large() { B.large_by_default = !B.large_by_default; (B.large_by_default ? s.show() : s.hide()); jQuery.cookie(COOKIE_NAME, (B.large_by_default ? 1 : 0)); } a.click(function(){ toggle_show_large(); return false; }); li.hover(function(){ div.show(); }, function(){ div.hide(); }); if (!B.large_by_default) s.hide(); })(jQuery, this); /* } */ /* In Opera 10.63, the img.complete property is not reset to false after changing the * src property. Blits from images to the canvas silently fail, with nothing being * blitted and no exception raised. This causes blank canvases to be displayed, because * we have no way of telling whether the image is blittable or if the blit succeeded. */ if(!Prototype.Browser.Opera) this.canvas = create_canvas_2d(); if(this.canvas) { this.canvas.hide(); this.img_box.appendChild(this.canvas); } this.zoom_level = 0; /* True if the post UI is visible. */ this.post_ui_visible = true; this.update_navigator = this.update_navigator.bind(this); Event.on(window, "resize", this.window_resize_event.bindAsEventListener(this)); document.on("viewer:vote", function(event) { if(this.vote_widget) this.vote_widget.set(event.memo.score); }.bindAsEventListener(this)); if(TagCompletion) TagCompletion.init(); /* Double-clicking the main image, or on nothing, toggles the thumb bar. */ this.container.down(".image-container").on("dblclick", ".image-container", function(event) { /* Watch out: Firefox fires dblclick events for all buttons, with the standard * button maps, but IE only fires it for left click and doesn't set button at * all, so event.isLeftClick won't work. */ if(event.button) return; event.stop(); document.fire("viewer:set-thumb-bar", {toggle: true}); }.bindAsEventListener(this)); /* Image controls: */ document.on("viewer:view-large-toggle", function(e) { this.toggle_view_large_image(); }.bindAsEventListener(this)); this.container.down(".post-info").on("click", ".toggle-zoom", function(e) { e.stop(); this.toggle_view_large_image(true); }.bindAsEventListener(this)); // Pop. this.container.down(".parent-post").down("A").on("click", this.parent_post_click_event.bindAsEventListener(this)); this.container.down(".child-posts").down("A").on("click", this.child_posts_click_event.bindAsEventListener(this)); this.container.down(".post-frames").on("click", ".post-frame-link", function(e, item) { e.stop(); /* Change the displayed post frame to the one that was clicked. Since all post frames * are usually displayed in the thumbnail view, set center_thumbs to true to recenter * on the thumb that was clicked, so it's clearer what's happening. */ document.fire("viewer:set-active-post", {post_id: this.displayed_post_id, post_frame: item.post_frame, center_thumbs: true}); }.bind(this)); /* We'll receive this message from the thumbnail view when the overlay is * visible on the bottom of the screen, to tell us how much space is covered up * by it. */ this.thumb_bar_height = 0; document.on("viewer:thumb-bar-changed", function(e) { /* Update the thumb bar height and rescale the image to fit the new area. */ this.thumb_bar_height = e.memo.height; this.update_image_window_size(); this.set_post_ui(e.memo.shown); this.scale_and_position_image(true); }.bindAsEventListener(this)); /* OnKey(79, null, function(e) { this.zoom_level -= 1; this.scale_and_position_image(true); this.update_navigator(); return true; }.bindAsEventListener(this)); OnKey(80, null, function(e) { this.zoom_level += 1; this.scale_and_position_image(true); this.update_navigator(); return true; }.bindAsEventListener(this)); */ /* Hide member-only and moderator-only controls: */ $(document.body).pickClassName("is-member", "not-member", User.is_member_or_higher()); $(document.body).pickClassName("is-moderator", "not-moderator", User.is_mod_or_higher()); var tag_span = this.container.down(".post-tags"); tag_span.on("click", ".post-tag", function(e, element) { e.stop(); document.fire("viewer:perform-search", {tags: element.tag_name}); }.bind(this)); /* These two links do the same thing, but one is shown to approve a pending post * and the other is shown to unflag a flagged post, so they prompt differently. */ this.container.down(".post-approve").on("click", function(e) { e.stop(); if(!confirm("Approve this post?")) return; var post_id = this.displayed_post_id; Post.approve(post_id, false); }.bindAsEventListener(this)); this.container.down(".post-unflag").on("click", function(e) { e.stop(); if(!confirm("Unflag this post?")) return; var post_id = this.displayed_post_id; Post.unflag(post_id); }.bindAsEventListener(this)); this.container.down(".post-delete").on("click", function(e) { e.stop(); var post = Post.posts.get(this.displayed_post_id); var default_reason = ""; if(post.flag_detail) default_reason = post.flag_detail.reason; var reason = prompt("Reason:", default_reason); if(!reason || reason == "") return; var post_id = this.displayed_post_id; Post.approve(post_id, reason); }.bindAsEventListener(this)); this.container.down(".post-undelete").on("click", function(e) { e.stop(); if(!confirm("Undelete this post?")) return; var post_id = this.displayed_post_id; Post.undelete(post_id); }.bindAsEventListener(this)); this.container.down(".flag-button").on("click", function(e) { e.stop(); var post_id = this.displayed_post_id; Post.flag(post_id); }.bindAsEventListener(this)); this.container.down(".activate-post").on("click", function(e) { e.stop(); var post_id = this.displayed_post_id; if(!confirm("Activate this post?")) return; Post.update_batch([{ id: post_id, is_held: false }], function() { var post = Post.posts.get(post_id); if(post.is_held) notice("Couldn't activate post"); else notice("Activated post"); }.bind(this)); }.bindAsEventListener(this)); this.container.down(".reparent-post").on("click", function(e) { e.stop(); if(!confirm("Make this post the parent?")) return; var post_id = this.displayed_post_id; var post = Post.posts.get(post_id); if(post == null) return; Post.reparent_post(post_id, post.parent_id, false); }.bindAsEventListener(this)); this.container.down(".pool-info").on("click", ".remove-pool-from-post", function(e, element) { e.stop(); var pool_info = element.up(".pool-info"); var pool = Pool.pools.get(pool_info.pool_id); var pool_name = pool.name.replace(/_/g, ' '); if(!confirm("Remove this post from pool #" + pool_info.pool_id + ": " + pool_name + "?")) return; Pool.remove_post(pool_info.post_id, pool_info.pool_id); }.bind(this)); /* Post editing: */ var post_edit = this.container.down(".post-edit"); post_edit.down("FORM").on("submit", function(e) { e.stop(); this.edit_save(); }.bindAsEventListener(this)); this.container.down(".show-tag-edit").on("click", function(e) { e.stop(); this.edit_show(true); }.bindAsEventListener(this)); this.container.down(".edit-save").on("click", function(e) { e.stop(); this.edit_save(); }.bindAsEventListener(this)); this.container.down(".edit-cancel").on("click", function(e) { e.stop(); this.edit_show(false); }.bindAsEventListener(this)); this.edit_post_area_changed = this.edit_post_area_changed.bind(this); post_edit.down(".edit-tags").on("paste", function(e) { this.edit_post_area_changed.defer(); }.bindAsEventListener(this)); post_edit.down(".edit-tags").on("keydown", function(e) { this.edit_post_area_changed.defer(); }.bindAsEventListener(this)); new TagCompletionBox(post_edit.down(".edit-tags")); this.container.down(".post-edit").on("keydown", function(e) { /* Don't e.stop() KEY_ESC, so we fall through and let handle_keypress unfocus the * form entry, if any. Otherwise, Chrome gets confused and leaves the focus on the * hidden input, where it'll steal keystrokes. */ if (e.keyCode == Event.KEY_ESC) { this.edit_show(false); } else if (e.keyCode == Event.KEY_RETURN) { e.stop(); this.edit_save(); } }.bindAsEventListener(this)); /* When the edit-post hotkey is pressed (E), force the post UI open and show editing. */ document.on("viewer:edit-post", function(e) { document.fire("viewer:set-thumb-bar", { set: true }); this.edit_show(true); }.bindAsEventListener(this)); /* When the post that's currently being displayed is updated by an API call, update * the displayed info. */ document.on("posts:update", function(e) { if(e.memo.post_ids.get(this.displayed_post_id) == null) return; this.set_post_info(); }.bindAsEventListener(this)); this.vote_widget = new Vote(jQuery(this.container.down(".vote-container"), null)); this.vote_widget.initShortcut(); this.blacklist_override_post_id = null; this.container.down(".show-blacklisted").on("click", function(e) { e.preventDefault(); }.bindAsEventListener(this)); this.container.down(".show-blacklisted").on("dblclick", function(e) { e.stop(); this.blacklist_override_post_id = this.displayed_post_id; var post = Post.posts.get(this.displayed_post_id); this.set_main_image(post, this.displayed_post_frame); }.bindAsEventListener(this)); this.img_box.on("viewer:center-on", function(e) { this.center_image_on(e.memo.x, e.memo.y); }.bindAsEventListener(this)); this.navigator = new Navigator(this.container.down(".image-navigator"), this.img_box); this.container.on("swipe:horizontal", function(e) { document.fire("viewer:show-next-post", { prev: e.memo.right }); }.bindAsEventListener(this)); // if(Prototype.BrowserFeatures.Touchscreen) /* MI: Using Prototype.BrowserIsMobile instead. */ if (Prototype.BrowserIsMobile) { this.create_voting_popup(); this.image_swipe = new SwipeHandler(this.container.down(".image-container")); } /* Create the frame editor. This must be created before image_dragger, since it takes priority * for drags. */ this.container.down(".edit-frames-button").on("click", function(e) { e.stop(); this.show_frame_editor(); }.bindAsEventListener(this)); this.frame_editor = new FrameEditor(this.container.down(".frame-editor"), this.img_box, this.container.down(".frame-editor-popup"), { onClose: function() { this.hide_frame_editor(); }.bind(this) }); /* If we're using dragging as a swipe gesture (see SwipeHandler), don't use it for * dragging too. */ if(this.image_swipe == null) this.image_dragger = new WindowDragElementAbsolute(this.img_box, this.update_navigator); } BrowserView.prototype.create_voting_popup = function() { /* Create the low-level voting widget. */ var popup_vote_widget_container = this.container.down(".vote-popup-container"); popup_vote_widget_container.show(); this.popup_vote_widget = new Vote(jQuery(popup_vote_widget_container), null); this.popup_vote_widget.initShortcut(); var flash = this.container.down(".vote-popup-flash"); /* vote-popup-expand is the part that's always present and is clicked to display the * voting popup. Create a dragger on it, and pass the position down to the voting * popup as we drag around. */ var popup_expand = this.container.down(".vote-popup-expand"); popup_expand.show(); var last_dragged_over = null; this.popup_vote_dragger = new DragElement(popup_expand, { ondown: function(drag) { /* Stop the touchdown/mousedown events, so this drag takes priority over any * others. In particular, we don't want this.image_swipe to also catch this * as a drag. */ drag.latest_event.stop(); flash.hide(); flash.removeClassName("flash-star"); this.popup_vote_widget.set_mouseover(null); last_dragged_over = null; popup_vote_widget_container.removeClassName("vote-popup-hidden"); }.bind(this), onup: function(drag) { /* If we're cancelling the drag, don't activate the vote, if any. */ if(drag.cancelling) { debug("cancelling drag"); last_dragged_over = null; } /* Call even if star_container is null or not a star, so we clear any mouseover. */ this.popup_vote_widget.set_mouseover(last_dragged_over); var star = this.popup_vote_widget.activate_item(last_dragged_over); /* If a vote was made, flash the vote star. */ if(star != null) { /* Set the star-# class to color the star. */ for(var i = 0; i < 4; ++i) flash.removeClassName("star-" + i); flash.addClassName("star-" + star); flash.show(); /* Center the element on the screen. */ var offset = this.image_window_size; var flash_x = offset.width/2 - flash.offsetWidth/2; var flash_y = offset.height/2 - flash.offsetHeight/2; flash.setStyle({left: flash_x + "px", top: flash_y + "px"}); flash.addClassName("flash-star"); } popup_vote_widget_container.addClassName("vote-popup-hidden"); last_dragged_over = null; }.bind(this), ondrag: function(drag) { last_dragged_over = document.elementFromPoint(drag.x, drag.y); this.popup_vote_widget.set_mouseover(last_dragged_over); }.bind(this) }); } BrowserView.prototype.set_post_ui = function(visible) { /* Disable the post UI by default on touchscreens; we don't have an interface * to toggle it. */ // if(Prototype.BrowserFeatures.Touchscreen) /* MI: Using Prototype.BrowserIsMobile instead. */ if (Prototype.BrowserIsMobile) visible = false; /* If we don't have a post displayed, always hide the post UI even if it's currently * shown. */ this.container.down(".post-info").show(visible && this.displayed_post_id != null); if(visible == this.post_ui_visible) return; this.post_ui_visible = visible; if(this.navigator) this.navigator.set_autohide(!visible); /* If we're hiding the post UI, cancel the post editor if it's open. */ if(!this.post_ui_visible) this.edit_show(false); } BrowserView.prototype.image_loaded_event = function(event) { /* Record that the image is completely available, so it can be blitted to the canvas. * This is different than img.complete, which is true if the image has completed downloading * but hasn't yet been decoded, so isn't yet completely available. This generally happens * if we query img.completed quickly after setting img.src and the image data is cached. */ this.img.fully_loaded = true; document.fire("viewer:displayed-image-loaded", { post_id: this.displayed_post_id, post_frame: this.displayed_post_frame }); this.update_canvas(); } /* Return true if last_preload_request includes [post_id, post_frame]. */ BrowserView.prototype.post_frame_list_includes = function(post_id_list, post_id, post_frame) { var found_preload = post_id_list.find(function(post) { return post[0] == post_id && post[1] == post_frame; }); return found_preload != null; } /* Begin preloading the HTML and images for the given post IDs. */ BrowserView.prototype.preload = function(post_ids) { /* We're being asked to preload post_ids. Only do this if it seems to make sense: if * the user is actually traversing posts that are being preloaded. Look at the previous * call to preload(). If it didn't include the current post, then skip the preload. */ var last_preload_request = this.last_preload_request; this.last_preload_request = post_ids; if(!this.post_frame_list_includes(last_preload_request, this.wanted_post_id, this.wanted_post_frame)) { // debug("skipped-preload(" + post_ids.join(",") + ")"); this.last_preload_request_active = false; return; } this.last_preload_request_active = true; // debug("preload(" + post_ids.join(",") + ")"); var new_preload_container = new PreloadContainer(); for(var i = 0; i < post_ids.length; ++i) { var post_id = post_ids[i][0]; var post_frame = post_ids[i][1]; var post = Post.posts.get(post_id); if(post_frame != -1) { var frame = post.frames[post_frame]; new_preload_container.preload(frame.url); } else new_preload_container.preload(post.sample_url); } /* If we already were preloading images, we created the new preloads before * deleting the old ones. That way, any images that are still being preloaded * won't be deleted and recreated, possibly causing the download to be interrupted * and resumed. */ if(this.preload_container) this.preload_container.destroy(); this.preload_container = new_preload_container; } BrowserView.prototype.load_post_id_data = function(post_id) { debug("load needed"); // If we already have a request in flight, don't start another; wait for the // first to finish. if(this.current_ajax_request != null) return; new Ajax.Request("/post.json", { parameters: { tags: "id:" + post_id, api_version: 2, filter: 1, include_tags: "1", include_votes: "1", include_pools: 1 }, method: "get", onCreate: function(resp) { this.current_ajax_request = resp.request; }.bind(this), onSuccess: function(resp) { if(this.current_ajax_request != resp.request) return; /* If no posts were returned, then the post ID we're looking up doesn't exist; * treat this as a failure. */ var resp = resp.responseJSON; this.success = resp.posts.length > 0; if(!this.success) { notice("Post #" + post_id + " doesn't exist"); return; } Post.register_resp(resp); }.bind(this), onComplete: function(resp) { if(this.current_ajax_request == resp.request) this.current_ajax_request = null; /* If the request failed and we were requesting wanted_post_id, don't keep trying. */ var success = resp.request.success() && this.success; if(!success && post_id == this.wanted_post_id) { /* As a special case, if the post we requested doesn't exist and we aren't displaying * anything at all, force the thumb bar open so we don't show nothing at all. */ if(this.displayed_post_id == null) document.fire("viewer:set-thumb-bar", {set: true}); return; } /* This will either load the post we just finished, or request data for the * one we want. */ this.set_post(this.wanted_post_id, this.wanted_post_frame); }.bind(this), onFailure: function(resp) { notice("Error " + resp.status + " loading post"); }.bind(this) }); } BrowserView.prototype.set_viewing_larger_version = function(b, from_toggle_zoom) // Pop. { this.viewing_larger_version = b; var post = Post.posts.get(this.displayed_post_id); var can_zoom = post != null && post.jpeg_url != post.sample_url; var viewing_larger_version = from_toggle_zoom || !this.large_by_default ? b : !b; // Pop. this.container.down(".zoom-icon-none").show(!can_zoom); this.container.down(".zoom-icon-in").show(can_zoom && !viewing_larger_version); // Pop. this.container.down(".zoom-icon-out").show(can_zoom && viewing_larger_version); // Pop. /* When we're on the regular version and we're on a touchscreen, disable drag * scrolling so we can use it to switch images instead. */ // if(Prototype.BrowserFeatures.Touchscreen && this.image_dragger) /* MI: Using Prototype.BrowserIsMobile instead. */ if (Prototype.BrowserIsMobile && this.image_dragger) this.image_dragger.set_disabled(!b); /* Only allow dragging to create new frames when not viewing the large version, * since we need to be able to drag the image. */ if(this.frame_editor) { this.frame_editor.set_drag_to_create(!b); this.frame_editor.set_show_corner_drag(!b); } } BrowserView.prototype.set_main_image = function(post, post_frame, from_toggle_zoom) // Pop. { /* * Clear the previous post, if any. Don't keep the old IMG around; create a new one, or * we may trigger long-standing memory leaks in WebKit, eg.: * https://bugs.webkit.org/show_bug.cgi?id=31253 * * This also helps us avoid briefly displaying the old image with the new dimensions, which * can otherwise take some hoop jumping to prevent. */ if(this.img != null) { this.img.stopObserving(); this.img.parentNode.removeChild(this.img); this.image_pool.release(this.img); this.img = null; } /* If this post is blacklisted, show a message instead of displaying it. */ var hide_post = Post.is_blacklisted(post.id) && post.id != this.blacklist_override_post_id; this.container.down(".blacklisted-message").show(hide_post); if(hide_post) return; this.img = this.image_pool.get(); this.img.className = "main-image"; if(this.canvas) this.canvas.hide(); this.img.show(); /* * Work around an iPhone bug. If a touchstart event is sent to this.img, and then * (due to a swipe gesture) we remove the image and replace it with a new one, no * touchend is ever delivered, even though it's the containing box listening to the * event. Work around this by setting the image to pointer-events: none, so clicks on * the image will actually be sent to the containing box directly. */ this.img.setStyle({pointerEvents: "none"}); this.img.on("load", this.image_loaded_event.bindAsEventListener(this)); if (!from_toggle_zoom && this.large_by_default) this.viewing_larger_version = true this.img.fully_loaded = false; if(post_frame != -1 && post_frame < post.frames.length) { var frame = post.frames[post_frame]; this.img.src = frame.url; this.img_box.original_width = frame.width; this.img_box.original_height = frame.height; this.img_box.show(); } else if(this.viewing_larger_version && post.jpeg_url) { this.img.src = post.jpeg_url; this.img_box.original_width = post.jpeg_width; this.img_box.original_height = post.jpeg_height; this.img_box.show(); } else if(!this.viewing_larger_version && post.sample_url) { this.img.src = post.sample_url; this.img_box.original_width = post.sample_width; this.img_box.original_height = post.sample_height; this.img_box.show(); } else { /* Having no sample URL is an edge case, usually from deleted posts. Keep the number * of code paths smaller by creating the IMG anyway, but not showing it. */ this.img_box.hide(); } this.container.down(".image-box").appendChild(this.img); if(this.viewing_larger_version) { this.navigator.set_image(post.preview_url, post.actual_preview_width, post.actual_preview_height); this.navigator.set_autohide(!this.post_ui_visible); } this.navigator.enable(this.viewing_larger_version); this.scale_and_position_image(); } /* * Display post_id. If post_frame is not null, set the specified frame. * * If no_hash_change is true, the UrlHash will not be updated to reflect the new post. * This should be used when this is called to load the post already reflected by the * URL hash. For example, the hash "#/pool:123" shows pool 123 in the thumbnails and * shows its first post in the view. It should *not* change the URL hash to reflect * the actual first post (eg. #12345/pool:123). This will insert an unwanted history * state in the browser, so the user has to go back twice to get out. * * no_hash_change should also be set when loading a state as a result of hashchange, * for similar reasons. */ BrowserView.prototype.set_post = function(post_id, post_frame, lazy, no_hash_change, replace_history) { if(post_id == null) throw "post_id must not be null"; /* If there was a lazy load pending, cancel it. */ this.cancel_lazily_load(); this.wanted_post_id = post_id; this.wanted_post_frame = post_frame; this.wanted_post_no_hash_change = no_hash_change; this.wanted_post_replace_history = replace_history; if(post_id == this.displayed_post_id && post_frame == this.displayed_post_frame) return; /* If a lazy load was requested and we're not yet loading the image for this post, * delay loading. */ var is_cached = this.last_preload_request_active && this.post_frame_list_includes(this.last_preload_request, post_id, post_frame); if(lazy && !is_cached) { this.lazy_load_timer = window.setTimeout(function() { this.lazy_load_timer = null; this.set_post(this.wanted_post_id, this.wanted_post_frame, false, this.wanted_post_no_hash_change, this.wanted_post_replace_history); }.bind(this), 500); return; } this.hide_frame_editor(); var post = Post.posts.get(post_id); if(post == null) { /* The post we've been asked to display isn't loaded. Request a load and come back. */ if(this.displayed_post_id == null) this.container.down(".post-info").hide(); this.load_post_id_data(post_id); return; } if(post_frame == null) { // If post_frame is unspecified and we have a frame, display the first. post_frame = this.get_default_post_frame(post_id); // We know what frame we actually want to display now, so update wanted_post_frame. this.wanted_post_frame = post_frame; } /* If post_frame doesn't exist, just display the main post. */ if(post_frame != -1 && post.frames.length <= post_frame) post_frame = -1; this.displayed_post_id = post_id; this.displayed_post_frame = post_frame; if(!no_hash_change) { var post_frame_hash = this.get_post_frame_hash(post, post_frame); UrlHash.set_deferred({"post-id": post_id, "post-frame": post_frame_hash}, replace_history); } this.set_viewing_larger_version(false); this.set_main_image(post, post_frame); if(this.vote_widget) { if (this.vote_widget.post_id) { Post.votes.set(this.vote_widget.post_id, this.vote_widget.data.vote); Post.posts.get(this.vote_widget.post_id).score = this.vote_widget.data.score; } this.vote_widget.post_id = post.id; this.vote_widget.updateWidget(Post.votes.get(post.id), post.score); }; if(this.popup_vote_widget) { this.popup_vote_widget.post_id = post.id; this.popup_vote_widget.updateWidget(Post.votes.get(post.id), post.score); }; document.fire("viewer:displayed-post-changed", { post_id: post_id, post_frame: post_frame }); this.set_post_info(); /* Hide the editor when changing posts. */ this.edit_show(false); } /* Return the frame spec for the hash, eg. "-0". * * If the post has no frames, then just omit the frame spec. If the post has any frames, * then return the frame number or "-F" for the full image. */ BrowserView.prototype.post_frame_hash = function(post, post_frame) { if(post.frames.length == 0) return ""; return "-" + (post_frame == -1? "F":post_frame); } /* Return the default frame to display for the given post. If the post isn't loaded, * we don't know which frame we'll display and null will be returned. This corresponds * to a hash of #1234, where no frame is specified (eg. #1234-F, #1234-0). */ BrowserView.prototype.get_default_post_frame = function(post_id) { var post = Post.posts.get(post_id); if(post == null) return null; return post.frames.length > 0? 0: -1; } BrowserView.prototype.get_post_frame_hash = function(post, post_frame) { /* * Omitting the frame in the hash selects the default frame: the first frame if any, * otherwise the full image. If we're setting the hash to a post_frame which would be * selected by this default, omit the frame so this default is used. For example, if * post #1234 has one frame and post_frame is 0, it would be selected by the default, * so omit the frame and use a hash of #1234, not #1234-0. * * This helps normalize the hash. Otherwise, loading /#1234 will update the hash to * /#1234-in set_post, causing an unwanted history entry. */ var default_frame = post.frames.length > 0? 0:-1; if(post_frame == default_frame) return null; else return post_frame; } /* Set the post info box for the currently displayed post. */ BrowserView.prototype.set_post_info = function() { var post = Post.posts.get(this.displayed_post_id); if(!post) return; this.container.down(".post-id").setTextContent(post.id); this.container.down(".post-id-link").href = "/post/show/" + post.id; this.container.down(".posted-by").show(); this.container.down(".posted-at").setTextContent(time_ago_in_words(new Date(post.created_at*1000))); /* Fill in the pool list. */ var pool_info = this.container.down(".pool-info"); while(pool_info.firstChild) pool_info.removeChild(pool_info.firstChild); if(post.pool_posts) { post.pool_posts.each(function(pp) { var pool_post = pp[1]; var pool_id = pool_post.pool_id; var pool = Pool.pools.get(pool_id); var pool_title = pool.name.replace(/_/g, " "); var sequence = pool_post.sequence; if(sequence.match(/^[0-9]/)) sequence = "#" + sequence; var html = '
'; var div = html.subst({ sequence: sequence, pool_id: pool_id, desc: pool_title.escapeHTML() }).createElement(); div.post_id = post.id; div.pool_id = pool_id; pool_info.appendChild(div); }.bind(this)); } if(post.creator_id != null) { this.container.down(".posted-by").down("A").href = "/user/show/" + post.creator_id; this.container.down(".posted-by").down("A").setTextContent(post.author); } else { this.container.down(".posted-by").down("A").href = "#" this.container.down(".posted-by").down("A").setTextContent('Anonymous'); } this.container.down(".post-dimensions").setTextContent(post.width + "x" + post.height); this.container.down(".post-source").show(post.source != ""); if(post.source != "") { var text = post.source; var url = null; var m = post.source.match(/^http:\/\/.*pixiv\.net\/(img\d+\/)?img\/(\w+)\/(\d+)(_.+)?\.\w+$/); if(m) { text = "pixiv #" + m[3] + " (" + m[2] + ")"; url = "http://www.pixiv.net/member_illust.php?mode=medium&illust_id=" + m[3]; } else if(post.source.substr(0, 7) == "http://") { text = text.substr(7); if(text.substr(0, 4) == "www.") text = text.substr(4); if(text.length > 20) text = text.substr(0, 20) + "..."; url = post.source; } var source_box = this.container.down(".post-source"); source_box.down("A").show(url != null); source_box.down("SPAN").show(url == null); if(url) { source_box.down("A").href = url; source_box.down("A").setTextContent(text); } else { source_box.down("SPAN").setTextContent(text); } } if(post.frames.length > 0) { /* Hide this with a class rather than by changing display, so show_frame_editor * and hide_frame_editor can hide and unhide this separately. */ this.container.down(".post-frames").removeClassName("no-frames"); var frame_list = this.container.down(".post-frame-list"); while(frame_list.firstChild) frame_list.removeChild(frame_list.firstChild); for(var i = -1; i < post.frames.length; ++i) { var text = i == -1? "main": (i+1); var a = document.createElement("a"); a.href = "/post/browse#" + post.id + this.post_frame_hash(post, i); a.className = "post-frame-link"; if(this.displayed_post_frame == i) a.className += " current-post-frame"; a.setTextContent(text); a.post_frame = i; frame_list.appendChild(a); } } else { this.container.down(".post-frames").addClassName("no-frames"); } var ratings = {s: "Safe", q: "Questionable", e: "Explicit"}; this.container.down(".post-rating").setTextContent(ratings[post.rating]); this.container.down(".post-score").setTextContent(post.score); this.container.down(".post-hidden").show(!post.is_shown_in_index); this.container.down(".post-info").show(this.post_ui_visible); var file_extension = function(path) { var m = path.match(/.*\.([^.]+)/); if(!m) return ""; return m[1]; } var has_sample = (post.sample_url != post.file_url); var has_jpeg = (post.jpeg_url != post.file_url); var has_image = post.file_url != null && !has_sample; /* Hide the whole download-links box if there are no downloads available, usually * because of a deleted post. */ this.container.down(".download-links").show(has_image || has_sample || has_jpeg); this.container.down(".download-image").show(has_image); if(has_image) { this.container.down(".download-image").href = post.file_url; this.container.down(".download-image-desc").setTextContent(number_to_human_size(post.file_size) + " " + file_extension(post.file_url.toUpperCase())); } this.container.down(".download-jpeg").show(has_sample); if(has_sample) { this.container.down(".download-jpeg").href = has_jpeg? post.jpeg_url: post.file_url; var image_desc = number_to_human_size(has_jpeg? post.jpeg_file_size: post.file_size) /*+ " " + post.jpeg_width + "x" + post.jpeg_height*/ + " JPG"; this.container.down(".download-jpeg-desc").setTextContent(image_desc); } this.container.down(".download-png").show(has_jpeg); if(has_jpeg) { this.container.down(".download-png").href = post.file_url; var png_desc = number_to_human_size(post.file_size) /*+ " " + post.width + "x" + post.height*/ + " " + file_extension(post.file_url.toUpperCase()); this.container.down(".download-png-desc").setTextContent(png_desc); } /* For links that are handled by click events, try to set the href so that copying the * link will give a similar effect. For example, clicking parent-post will call set_post * to display it, and the href links to /post/browse#12345. */ var parent_post = this.container.down(".parent-post"); parent_post.show(post.parent_id != null); if(post.parent_id) parent_post.down("A").href = "/post/browse#" + post.parent_id; var child_posts = this.container.down(".child-posts"); child_posts.show(post.has_children); if(post.has_children) child_posts.down("A").href = "/post/browse#/parent:" + post.id; /* Create the tag links. */ var tag_span = this.container.down(".post-tags"); var first = true; while(tag_span.firstChild) tag_span.removeChild(tag_span.firstChild); var tags_by_type = Post.get_post_tags_with_type(post); tags_by_type.each(function(t) { var tag = t[0]; var type = t[1]; var span = $(document.createElement("SPAN", "")); span = $(span); span.className = "tag-type-" + type; var space = document.createTextNode(" "); span.appendChild(space); var a = jQuery('', { text: tag, href: "/post/browse#/" + tag, class: "post-tag tag-type-" + type, }); /* Break tags with