From f7d99ee5fc63cc382b64d4603946b11dc389b48a Mon Sep 17 00:00:00 2001 From: aneuhmanh Date: Thu, 3 Apr 2025 10:53:55 +0300 Subject: [PATCH] Videos page new design --- app.py | 105 +++++++++++++------- static/css/styles.scss | 188 +++++++++++++++++++++++++++-------- static/js/hoverPreview.js | 64 ++++++++++++ templates/navbar.html | 3 +- templates/subnav.html | 1 - templates/tags_list.html | 1 - templates/videos.html | 199 +++++++++++++++++++++----------------- 7 files changed, 388 insertions(+), 173 deletions(-) create mode 100644 static/js/hoverPreview.js diff --git a/app.py b/app.py index 5f98e03..57fe59c 100644 --- a/app.py +++ b/app.py @@ -40,7 +40,7 @@ app.config.from_object(Config) db.init_app(app) bcrypt.init_app(app) login_manager = LoginManager(app) -login_manager.login_view = 'login' +login_manager.login_view = 'auth.login' @login_manager.user_loader def load_user(user_id): @@ -114,8 +114,6 @@ def vote_art(image_id): return redirect(url_for('view', content_type='art', id=image_id)) -@app.route('/view//', methods=['GET', 'POST']) - @app.route('/view//', methods=['GET', 'POST']) def view(content_type, id): comments = [] @@ -131,6 +129,13 @@ def view(content_type, id): content = Image.query.get_or_404(id) comments = Comments.query.filter_by(image_id=id).order_by(Comments.comment_date.desc()).all() + if current_user.is_authenticated: + existing_view = Views.query.filter_by(image_id=id, username=current_user.username).first() + if not existing_view: + new_view = Views(image_id=id, username=current_user.username) + db.session.add(new_view) + db.session.commit() + search_query = request.args.get('search') page = request.args.get('page', 1, type=int) subscriptions = [] @@ -157,6 +162,13 @@ def view(content_type, id): content = Video.query.get_or_404(id) comments = Comments.query.filter_by(video_id=id).order_by(Comments.comment_date.desc()).all() + if current_user.is_authenticated: + existing_view = Views.query.filter_by(video_id=id, username=current_user.username).first() + if not existing_view: + new_view = Views(video_id=id, username=current_user.username) + db.session.add(new_view) + db.session.commit() + all_videos = Video.query.order_by(Video.id).all() video_ids = [video.id for video in all_videos] current_index = video_ids.index(id) @@ -172,6 +184,13 @@ def view(content_type, id): content = Comic.query.get_or_404(id) comments = Comments.query.filter_by(comic_id=id).order_by(Comments.comment_date.desc()).all() + if current_user.is_authenticated: + existing_view = Views.query.filter_by(image_id=id, username=current_user.username).first() + if not existing_view: + new_view = Views(image_id=id, username=current_user.username) + db.session.add(new_view) + db.session.commit() + comic_pages = ComicPage.query.filter_by(comic_id=id).order_by(ComicPage.page_number).all() if not comic_pages: @@ -286,8 +305,7 @@ def image_edit(id): @app.route('/video_edit/', methods=['GET', 'POST']) @login_required -def video_edit(id): - +async def video_edit(id): video = Video.query.get_or_404(id) if video.username != current_user.username: @@ -301,7 +319,6 @@ def video_edit(id): form.tags.data = video.tags if form.validate_on_submit(): - video.video_name = form.video_name.data video.description = form.description.data video.tags = form.tags.data @@ -310,10 +327,15 @@ def video_edit(id): thumbnail_file = form.video_thumbnail.data if allowed_file(thumbnail_file.filename, app.config['ALLOWED_IMAGE_EXTENSIONS']): - thumbnail_filename = generate_unique_filename(thumbnail_file.filename, app.config['UPLOAD_FOLDER']['thumbnails']) + thumbnail_filename = await generate_unique_filename( + app.config['UPLOAD_FOLDER']['thumbnails'], 'webp' + ) thumbnail_path = os.path.join(app.config['UPLOAD_FOLDER']['thumbnails'], thumbnail_filename) - thumbnail_file.save(thumbnail_path) + webp_thumbnail = await convert_to_webp(thumbnail_file) + async with aiofiles.open(thumbnail_path, 'wb') as f: + await f.write(webp_thumbnail.read()) + video.video_thumbnail_file = thumbnail_filename db.session.commit() @@ -476,6 +498,40 @@ def videos(): query = get_content_query(Video, subscriptions, search_query) pagination = query.paginate(page=page, per_page=10, error_out=False) + videos_with_views = [] + for video in pagination.items: + views_count = db.session.query(func.count(Views.id)).filter(Views.video_id == video.id).scalar() + videos_with_views.append({ + 'video': video, + 'views_count': views_count + }) + + popular_videos = [ + { + 'video': video[0], + 'views_count': video[1] + } + for video in db.session.query(Video, func.count(Views.id).label('views_count')) + .outerjoin(Views, Views.video_id == Video.id) + .group_by(Video.id) + .order_by(func.count(Views.id).desc(), Video.cookie_votes.desc()) + .limit(8) + .all() + ] + + most_viewed_videos = [ + { + 'video': video[0], + 'views_count': video[1] + } + for video in db.session.query(Video, func.count(Views.id).label('views_count')) + .outerjoin(Views, Views.video_id == Video.id) + .group_by(Video.id) + .order_by(func.count(Views.id).desc()) + .limit(8) + .all() + ] + videos_tags = [video.tags for video in Video.query.all() if video.tags] all_tags = [tag.strip() for tags in videos_tags for tag in tags.split(',')] sorted_tags = sorted(set(all_tags)) @@ -487,12 +543,14 @@ def videos(): return render_template( 'videos.html', - videos=pagination.items, + videos=videos_with_views, pagination=pagination, user_cookies=user_cookies, search_query=search_query, content_type='video', - tags=sorted_tags + tags=sorted_tags, + popular_videos=popular_videos, + most_viewed_videos=most_viewed_videos ) @@ -889,33 +947,6 @@ def terms_of_use(): def publication_rules(): return render_template('publication_rules.html') -@app.route('/shop') -@login_required -def shop(): - - items = Item.query.filter_by(visible=True).all() - user_cookies = Cookies.query.filter_by(username=current_user.username).first().cookies if Cookies.query.filter_by(username=current_user.username).first() else 0 - user_item_ids = {ui.item_id for ui in UserItem.query.filter_by(username=current_user.username).all()} - return render_template('shop.html', items=items, user=current_user, user_cookies=user_cookies, user_item_ids=user_item_ids) - -@app.route('/buy_item/', methods=['POST']) -@login_required -def buy_item(item_id): - username = current_user.username - user_cookies = Cookies.query.filter_by(username=username).first() - item = Item.query.get(item_id) - - if not user_cookies or not item or not item.visible or user_cookies.cookies < item.price: - return redirect(url_for('shop')) - - if UserItem.query.filter_by(username=username, item_id=item.id).first(): - return redirect(url_for('shop')) - - user_cookies.cookies -= item.price - db.session.add(UserItem(username=username, item_id=item.id)) - db.session.commit() - return redirect(url_for('shop')) - if __name__ == '__main__': with app.app_context(): db.create_all() diff --git a/static/css/styles.scss b/static/css/styles.scss index c39066f..233f15a 100644 --- a/static/css/styles.scss +++ b/static/css/styles.scss @@ -1,12 +1,14 @@ $dark-violet: #0D0C1C; -$violet: #3c3882; +$violet: #3C3882; $light-violet: #8784C9; @import url('https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200..1000;1,200..1000&family=Playwrite+IT+Moderna:wght@100..400&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap'); -body{ + +body { background-color: #05040A; font-family: Nunito, sans-serif; } + .container { position: absolute; display: flex; @@ -31,6 +33,7 @@ body{ padding: 0; box-sizing: border-box; } + .img-new-content { width: 1502px; height: 423px; @@ -54,9 +57,11 @@ body{ gap: 20px; width: 1502px; height: 631px; - left: 209px; - position: absolute; top: 653px; + left: 50%; + transform: translateX(-50%); + position: absolute; + box-sizing: border-box; } .img-popular-content { @@ -80,10 +85,13 @@ body{ gap: 20px; width: 1502px; height: 631px; - left: 209px; - position: absolute; top: 1340px; + left: 50%; + transform: translateX(-50%); + position: absolute; + box-sizing: border-box; } + .img-viewed-content { display: flex; flex-direction: column; @@ -97,7 +105,6 @@ body{ top: 1617px; } - .popular-categories { display: flex; justify-content: space-between; @@ -108,9 +115,11 @@ body{ box-sizing: border-box; position: absolute; top: 2095px; - left: 210px; + left: 50%; + transform: translateX(-50%); gap: 20px; } + .img-popular-categories { display: flex; justify-content: space-between; @@ -196,7 +205,7 @@ body{ } .view-more-button:hover { - background-color: #3C3882; + background-color: $violet; transition: background-color 0.3s ease, color 0.3s ease; } @@ -229,14 +238,13 @@ body{ } .img-view-more-button:hover { - background-color: #3C3882; + background-color: $violet; transition: background-color 0.3s ease, color 0.3s ease; } .img-view-more-button:hover .new-context-button-text { color: $light-violet; transition: color 0.3s ease; - } .new-context-button-text { @@ -246,9 +254,11 @@ body{ font-size: 16px; line-height: 100%; letter-spacing: 0%; - color: #3C3882; + color: $violet; } +/* navbar */ + .navbar { width: 100%; max-width: 1500px; @@ -259,16 +269,19 @@ body{ padding: 0 20px; gap: 20px; } + .navbar-wrapper { width: 100%; background: $dark-violet; display: flex; justify-content: center; } + .logo { width: 307px; height: 60px; } + .search-container { display: flex; align-items: center; @@ -281,12 +294,14 @@ body{ position: relative; transition: border-color 0.3s ease; } + .search-icon-container { position: relative; width: 24px; height: 24px; margin-right: 10px; } + .search-icon, .search-hover-icon { width: 24px; @@ -296,9 +311,11 @@ body{ left: 0; transition: opacity 0.3s ease; } + .search-hover-icon { opacity: 0; } + .search-container:hover .search-hover-icon, .search-container:focus-within .search-hover-icon { opacity: 1; @@ -320,11 +337,11 @@ body{ } .search-container:hover { - border-color: #3C3882; + border-color: $violet; } .search-container:focus-within { - border-color: #3C3882; + border-color: $violet; } .icon-container { @@ -338,11 +355,13 @@ body{ height: 24px; margin-left: 10px; } + .tray-icon { width: 11px; height: 7px; margin-left: 5px; } + .translate-btn { position: relative; width: 23px; @@ -357,6 +376,7 @@ body{ padding: 15px; transition: border-color 0.3s ease, transform 0.3s ease, opacity 0.3s ease; } + .translate-icon, .translate-hover-icon { width: 24px; @@ -367,9 +387,11 @@ body{ transform: translate(-50%, -50%); transition: opacity 0.3s ease; } + .translate-hover-icon { opacity: 0; } + .translate-btn:hover .translate-hover-icon { opacity: 1; } @@ -377,9 +399,11 @@ body{ .translate-btn:hover .translate-icon { opacity: 0; } + .translate-btn:hover { - border-color: #3C3882; + border-color: $violet; } + .overlay-icon { position: absolute; top: 50%; @@ -389,6 +413,7 @@ body{ height: 20px; opacity: 0.7; } + .menu { display: flex; gap: 22px; @@ -398,25 +423,30 @@ body{ padding-left: 60px; padding-right: 60px; } + .menu a { text-decoration: none; color: $light-violet; font-size: 16px; transition: color 0.3s ease; } + .menu a:hover { - color: #3C3882; + color: $violet; } + .auth-container { display: flex; align-items: center; margin-left: auto; } + .discord-icon-container { position: relative; width: 42px; height: 42px; } + .discord-icon, .discord-hover-icon { width: 42px; @@ -426,15 +456,19 @@ body{ left: 0; transition: opacity 0.3s ease; } + .discord-hover-icon { opacity: 0; } + .discord-icon-container:hover .discord-hover-icon { opacity: 1; } + .discord-icon-container:hover .discord-icon { opacity: 0; } + .login-btn { width: 87px; height: 42px; @@ -451,42 +485,50 @@ body{ margin-left: 10px; transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; } + .login-btn:hover { - background-color: #3C3882; + background-color: $violet; color: $dark-violet; - border-color: #3C3882; + border-color: $violet; } + .dropdown-menu { -position: absolute; -top: 100%; -right: 0; -background: $dark-violet; -border: 1px solid #3C3882; -border-radius: 8px; -box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); -display: none; -flex-direction: column; -width: 150px; -z-index: 10; + position: absolute; + top: 100%; + right: 0; + background: $dark-violet; + border: 1px solid $violet; + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + display: none; + flex-direction: column; + width: 150px; + z-index: 10; } + .dropdown-item { -padding: 10px 15px; -font-size: 14px; -border-radius:4px; -color: $light-violet; -cursor: pointer; -transition: background 0.3s ease, color 0.3s ease; + padding: 10px 15px; + font-size: 14px; + border-radius: 4px; + color: $light-violet; + cursor: pointer; + transition: background 0.3s ease, color 0.3s ease; } + .dropdown-item:hover { -background: #3C3882; -color: white; + background: $violet; + color: white; } + +/* tags-list */ + .tags-container { width: 1500px; height: 35px; position: absolute; top: 192px; - left: 210px; + left: 50%; + transform: translateX(-50%); display: flex; justify-content: flex-start; align-items: center; @@ -509,7 +551,7 @@ color: white; display: inline-flex; align-items: center; justify-content: center; - background-color: #3C3882; + background-color: $violet; border: none; color: $light-violet; margin: 0; @@ -527,7 +569,7 @@ color: white; justify-content: center; border: 2px solid $light-violet; background-color: transparent; - color: #3C3882; + color: $violet; margin-left: 10px; gap: 5px; position: relative; @@ -541,6 +583,9 @@ color: white; right: 0; background: linear-gradient(to right, rgba(5, 4, 10, 0) 30%, rgba(5, 4, 10, 0.5) 60%, #05040A 100%); } + +/* cards */ + .img-cards-grid { display: grid; grid-template-columns: repeat(6, 1fr); @@ -549,6 +594,7 @@ color: white; height: 708px; margin-top: 60px; } + .img-card { width: 225px; height: 344px; @@ -556,6 +602,7 @@ color: white; flex-direction: column; gap: 5px; } + .img-card-cover { width: 100%; height: 280px; @@ -577,12 +624,35 @@ color: white; display: flex; flex-direction: column; gap: 5px; + overflow: hidden; } .card-cover { width: 100%; height: 180px; background: #1D1C2E; + position: relative; + overflow: hidden; +} + +.card-cover img { + width: 100%; + height: 100%; + object-fit: cover; + position: absolute; + top: 0; + left: 0; +} + +.card-cover video.preview-video { + width: 100%; + height: 100%; + object-fit: cover; + position: absolute; + top: 0; + left: 0; + display: none; + z-index: 1; } .card-info { @@ -614,6 +684,7 @@ color: white; font-size: 14px; color: $light-violet; } + .img-small-cards-grid { display: grid; grid-template-columns: repeat(7, 1fr); @@ -621,11 +692,14 @@ color: white; width: 100%; margin-top: 60px; } + .img-small-card-cover { width: 189px; height: 250px; background: #1D1C2E; } + + .img-small-card { width: 189px; height: 314px; @@ -633,6 +707,7 @@ color: white; flex-direction: column; gap: 5px; } + .small-cards-grid { display: grid; grid-template-columns: repeat(5, 1fr); @@ -647,12 +722,35 @@ color: white; display: flex; flex-direction: column; gap: 5px; + overflow: hidden; } .small-card-cover { width: 100%; height: 163px; background: #1D1C2E; + position: relative; + overflow: hidden; +} + +.small-card-cover img { + width: 100%; + height: 100%; + object-fit: cover; + position: absolute; + top: 0; + left: 0; +} + +.small-card-cover video.preview-video { + width: 100%; + height: 100%; + object-fit: cover; + position: absolute; + top: 0; + left: 0; + display: none; + z-index: 1; } .small-card-info { @@ -684,6 +782,7 @@ color: white; font-size: 14px; color: $light-violet; } + .pc-card { width: 233px; height: 164px; @@ -724,10 +823,10 @@ color: white; font-size: 14px; line-height: 100%; letter-spacing: 0%; - color: #3C3882; + color: $violet; text-align: right; position: relative; - right:5px; + right: 5px; } .ac-img-small-cards-grid { @@ -794,7 +893,7 @@ color: white; width: 276px; height: 40px; border-radius: 10px; - background-color: #3C3882; + background-color: $violet; color: white; border: none; cursor: pointer; @@ -813,6 +912,9 @@ color: white; letter-spacing: 0%; white-space: nowrap; } + +/* pagination */ + .pagination-container { width: 626px; height: 50px; diff --git a/static/js/hoverPreview.js b/static/js/hoverPreview.js new file mode 100644 index 0000000..3422376 --- /dev/null +++ b/static/js/hoverPreview.js @@ -0,0 +1,64 @@ +document.addEventListener('DOMContentLoaded', () => { + const cardCovers = document.querySelectorAll('.small-card-cover, .card-cover'); + + cardCovers.forEach(cardCover => { + const thumbnail = cardCover.querySelector('.thumbnail'); + const video = cardCover.querySelector('.preview-video'); + const videoSource = video.querySelector('source'); + let hoverTimeout; + + cardCover.addEventListener('mouseenter', () => { + hoverTimeout = setTimeout(() => { + + if (!videoSource.src) { + videoSource.src = video.dataset.src; + video.load(); + } + + thumbnail.style.display = 'none'; + video.style.display = 'block'; + + video.addEventListener('loadedmetadata', () => { + + if (video.duration > 15) { + + video.currentTime = 10; + const loopSegment = () => { + if (video.currentTime >= 15) { + video.currentTime = 10; + video.play(); + } + }; + video.addEventListener('timeupdate', loopSegment); + cardCover.loopSegment = loopSegment; + } else { + + video.currentTime = 0; + const loopEntireVideo = () => { + if (video.currentTime >= video.duration) { + video.currentTime = 0; + video.play(); + } + }; + video.addEventListener('timeupdate', loopEntireVideo); + cardCover.loopSegment = loopEntireVideo; + } + + video.play(); + }); + }, 2000); + }); + + cardCover.addEventListener('mouseleave', () => { + clearTimeout(hoverTimeout); + video.pause(); + video.style.display = 'none'; + thumbnail.style.display = 'block'; + + if (cardCover.loopSegment) { + video.removeEventListener('timeupdate', cardCover.loopSegment); + delete cardCover.loopSegment; + } + }); + }); +}); \ No newline at end of file diff --git a/templates/navbar.html b/templates/navbar.html index 5d2e295..ca649aa 100644 --- a/templates/navbar.html +++ b/templates/navbar.html @@ -3,7 +3,6 @@ - Navbar @@ -46,7 +45,7 @@ Discord Hover - + diff --git a/templates/subnav.html b/templates/subnav.html index db698f3..e91223a 100644 --- a/templates/subnav.html +++ b/templates/subnav.html @@ -3,7 +3,6 @@ - Subnav