소스 검색

Merge pull request #5329 from pixelfed/staging

Add Profile Carousels
daniel 9 달 전
부모
커밋
91e2ad6415
79개의 변경된 파일652개의 추가작업 그리고 2개의 파일을 삭제
  1. 1 0
      CHANGELOG.md
  2. 8 2
      app/Http/Controllers/ProfileController.php
  3. 6 0
      package-lock.json
  4. 1 0
      package.json
  5. BIN
      public/css/profile.css
  6. BIN
      public/js/account-import.js
  7. BIN
      public/js/admin.js
  8. BIN
      public/js/admin_invite.js
  9. BIN
      public/js/app.js
  10. BIN
      public/js/changelog.bundle.da47c74f7034447a.js
  11. BIN
      public/js/collections.js
  12. BIN
      public/js/components.js
  13. BIN
      public/js/compose-classic.js
  14. BIN
      public/js/compose.chunk.34ebded0861594ef.js
  15. BIN
      public/js/compose.js
  16. BIN
      public/js/daci.chunk.5bb69fda8fdedc47.js
  17. BIN
      public/js/daci.chunk.5dbd1faea828ed0b.js
  18. BIN
      public/js/developers.js
  19. BIN
      public/js/direct.js
  20. BIN
      public/js/discover.chunk.b5e4952e4d62342b.js
  21. BIN
      public/js/discover~findfriends.chunk.2392a288a9031530.js
  22. BIN
      public/js/discover~findfriends.chunk.f3e42b724965b63c.js
  23. BIN
      public/js/discover~hashtag.bundle.94de7a1013d118bf.js
  24. BIN
      public/js/discover~memories.chunk.398b63b5473c6be6.js
  25. BIN
      public/js/discover~memories.chunk.c59b92ebe85d45cf.js
  26. BIN
      public/js/discover~myhashtags.chunk.29b97f80a1338877.js
  27. BIN
      public/js/discover~myhashtags.chunk.f068eadda9058061.js
  28. BIN
      public/js/discover~serverfeed.chunk.9d3ce36a12533da5.js
  29. BIN
      public/js/discover~serverfeed.chunk.d729660f46f7f530.js
  30. BIN
      public/js/discover~settings.chunk.61e5007e3e383807.js
  31. BIN
      public/js/discover~settings.chunk.6f4b9b6a6ef5131a.js
  32. BIN
      public/js/dms.chunk.49ae3599d4dba309.js
  33. BIN
      public/js/dms~message.chunk.61293d7251878a18.js
  34. BIN
      public/js/error404.bundle.9200c0b8734654fb.js
  35. BIN
      public/js/group-status.js
  36. BIN
      public/js/group-topic-feed.js
  37. BIN
      public/js/group.create.e6f580f22769b687.js
  38. BIN
      public/js/groups-page-about.f104deafd36d2813.js
  39. BIN
      public/js/groups-page-media.660d310e20bb9451.js
  40. BIN
      public/js/groups-page-members.9e6e807b47585ba8.js
  41. BIN
      public/js/groups-page-topics.d51e24af2273e3c4.js
  42. BIN
      public/js/groups-page.acb1312c8fa28603.js
  43. BIN
      public/js/groups-profile.2d5b53d784146dd1.js
  44. BIN
      public/js/groups.js
  45. BIN
      public/js/hashtag.js
  46. BIN
      public/js/home.chunk.1d1e6fe050aaaf98.js
  47. BIN
      public/js/home.chunk.491ce6f986e08bb3.js
  48. 0 0
      public/js/home.chunk.491ce6f986e08bb3.js.LICENSE.txt
  49. BIN
      public/js/i18n.bundle.873216ad86c80486.js
  50. BIN
      public/js/landing.js
  51. BIN
      public/js/manifest.js
  52. BIN
      public/js/notifications.chunk.f04bf557f846d93a.js
  53. BIN
      public/js/portfolio.js
  54. BIN
      public/js/post.chunk.13919bcfbfc2d438.js
  55. 0 0
      public/js/post.chunk.13919bcfbfc2d438.js.LICENSE.txt
  56. BIN
      public/js/post.chunk.af21320999ba64af.js
  57. BIN
      public/js/profile.chunk.164b255884ed6d1c.js
  58. BIN
      public/js/profile.chunk.75eb020992ddb4dd.js
  59. BIN
      public/js/profile.js
  60. 1 0
      public/js/profile.js.LICENSE.txt
  61. BIN
      public/js/profile~followers.bundle.fa171ea239061d55.js
  62. BIN
      public/js/profile~following.bundle.7a592645bb9eb11f.js
  63. BIN
      public/js/remote_auth.js
  64. BIN
      public/js/search.js
  65. BIN
      public/js/spa.js
  66. BIN
      public/js/status.js
  67. BIN
      public/js/stories.js
  68. BIN
      public/js/story-compose.js
  69. BIN
      public/js/timeline.js
  70. BIN
      public/js/vendor.js
  71. 6 0
      public/js/vendor.js.LICENSE.txt
  72. BIN
      public/mix-manifest.json
  73. 336 0
      resources/assets/components/FullscreenCarousel.vue
  74. 197 0
      resources/assets/components/ProfileCarousel.vue
  75. 46 0
      resources/assets/components/SplashScreen.vue
  76. 5 0
      resources/assets/js/profile.js
  77. 2 0
      resources/assets/sass/profile.scss
  78. 42 0
      resources/views/profile/show_carousel.blade.php
  79. 1 0
      webpack.mix.js

+ 1 - 0
CHANGELOG.md

@@ -6,6 +6,7 @@
 - Implement Admin Domain Blocks API (Mastodon API Compatible) [ThisIsMissEm](https://github.com/ThisIsMissEm) ([#5021](https://github.com/pixelfed/pixelfed/pull/5021))
 - Authorize Interaction support (for handling remote interactions) ([4ca7c6c3](https://github.com/pixelfed/pixelfed/commit/4ca7c6c3))
 - Contact Form Admin Responses ([52cc6090](https://github.com/pixelfed/pixelfed/commit/52cc6090))
+- Profile Carousels ([8af77a3f](https://github.com/pixelfed/pixelfed/commit/8af77a3f))
 
 ### Federation
 - Add ActiveSharedInboxService, for efficient sharedInbox caching ([1a6a3397](https://github.com/pixelfed/pixelfed/commit/1a6a3397))

+ 8 - 2
app/Http/Controllers/ProfileController.php

@@ -33,7 +33,7 @@ class ProfileController extends Controller
         }
 
         // redirect authed users to Metro 2.0
-        if ($request->user()) {
+        if ($request->user() && !$request->filled('carousel')) {
             // unless they force static view
             if (! $request->has('fs') || $request->input('fs') != '1') {
                 $pid = AccountService::usernameToId($username);
@@ -64,6 +64,7 @@ class ProfileController extends Controller
 
     protected function buildProfile(Request $request, $user)
     {
+        $carousel = (bool) $request->filled('carousel');
         $username = $user->username;
         $loggedIn = Auth::check();
         $isPrivate = false;
@@ -97,6 +98,9 @@ class ProfileController extends Controller
                 ],
             ];
 
+            if($carousel) {
+                return view('profile.show_carousel', compact('profile', 'settings'));
+            }
             return view('profile.show', compact('profile', 'settings'));
         } else {
             $key = 'profile:settings:'.$user->id;
@@ -135,7 +139,9 @@ class ProfileController extends Controller
                     'list' => $settings->show_profile_followers,
                 ],
             ];
-
+            if($carousel) {
+                return view('profile.show_carousel', compact('profile', 'settings'));
+            }
             return view('profile.show', compact('profile', 'settings'));
         }
     }

+ 6 - 0
package-lock.json

@@ -7,6 +7,7 @@
 			"name": "pixelfed",
 			"dependencies": {
 				"@fancyapps/fancybox": "^3.5.7",
+				"@glidejs/glide": "^3.6.2",
 				"@hcaptcha/vue-hcaptcha": "^1.3.0",
 				"@peertube/p2p-media-loader-core": "^1.0.14",
 				"@peertube/p2p-media-loader-hlsjs": "^1.0.14",
@@ -2140,6 +2141,11 @@
 				"jquery": ">=1.9.0"
 			}
 		},
+		"node_modules/@glidejs/glide": {
+			"version": "3.6.2",
+			"resolved": "https://registry.npmjs.org/@glidejs/glide/-/glide-3.6.2.tgz",
+			"integrity": "sha512-oXw7In0IZV69PC0PChQakY+yh+UnqIb5+zfVuEIzub6Kkfl1foo7TAhr2PZXPzihOG9YS57t8wvdzBFEZ0aPVA=="
+		},
 		"node_modules/@hcaptcha/vue-hcaptcha": {
 			"version": "1.3.0",
 			"resolved": "https://registry.npmjs.org/@hcaptcha/vue-hcaptcha/-/vue-hcaptcha-1.3.0.tgz",

+ 1 - 0
package.json

@@ -34,6 +34,7 @@
 	},
 	"dependencies": {
 		"@fancyapps/fancybox": "^3.5.7",
+		"@glidejs/glide": "^3.6.2",
 		"@hcaptcha/vue-hcaptcha": "^1.3.0",
 		"@peertube/p2p-media-loader-core": "^1.0.14",
 		"@peertube/p2p-media-loader-hlsjs": "^1.0.14",

BIN
public/css/profile.css


BIN
public/js/account-import.js


BIN
public/js/admin.js


BIN
public/js/admin_invite.js


BIN
public/js/app.js


BIN
public/js/changelog.bundle.f4870a4224d34715.js → public/js/changelog.bundle.da47c74f7034447a.js


BIN
public/js/collections.js


BIN
public/js/components.js


BIN
public/js/compose-classic.js


BIN
public/js/compose.chunk.52e7225ee6c74bad.js → public/js/compose.chunk.34ebded0861594ef.js


BIN
public/js/compose.js


BIN
public/js/daci.chunk.5bb69fda8fdedc47.js


BIN
public/js/daci.chunk.5dbd1faea828ed0b.js


BIN
public/js/developers.js


BIN
public/js/direct.js


BIN
public/js/discover.chunk.4b677d22775c2e13.js → public/js/discover.chunk.b5e4952e4d62342b.js


BIN
public/js/discover~findfriends.chunk.2392a288a9031530.js


BIN
public/js/discover~findfriends.chunk.f3e42b724965b63c.js


BIN
public/js/discover~hashtag.bundle.2ab0025b9827bd11.js → public/js/discover~hashtag.bundle.94de7a1013d118bf.js


BIN
public/js/discover~memories.chunk.398b63b5473c6be6.js


BIN
public/js/discover~memories.chunk.c59b92ebe85d45cf.js


BIN
public/js/discover~myhashtags.chunk.29b97f80a1338877.js


BIN
public/js/discover~myhashtags.chunk.f068eadda9058061.js


BIN
public/js/discover~serverfeed.chunk.9d3ce36a12533da5.js


BIN
public/js/discover~serverfeed.chunk.d729660f46f7f530.js


BIN
public/js/discover~settings.chunk.61e5007e3e383807.js


BIN
public/js/discover~settings.chunk.6f4b9b6a6ef5131a.js


BIN
public/js/dms.chunk.1768ba82cee30612.js → public/js/dms.chunk.49ae3599d4dba309.js


BIN
public/js/dms~message.chunk.5f4e1fc636c70a14.js → public/js/dms~message.chunk.61293d7251878a18.js


BIN
public/js/error404.bundle.62723d31c3df8cfa.js → public/js/error404.bundle.9200c0b8734654fb.js


BIN
public/js/group-status.js


BIN
public/js/group-topic-feed.js


BIN
public/js/group.create.2fb3882ca480a0a2.js → public/js/group.create.e6f580f22769b687.js


BIN
public/js/groups-page-about.717e51a079fedb67.js → public/js/groups-page-about.f104deafd36d2813.js


BIN
public/js/groups-page-media.60587cd38c4cd089.js → public/js/groups-page-media.660d310e20bb9451.js


BIN
public/js/groups-page-members.e7866ecfe1fad40e.js → public/js/groups-page-members.9e6e807b47585ba8.js


BIN
public/js/groups-page-topics.b78ab423f7174566.js → public/js/groups-page-topics.d51e24af2273e3c4.js


BIN
public/js/groups-page.86e723ad211c456d.js → public/js/groups-page.acb1312c8fa28603.js


BIN
public/js/groups-profile.425f35dbfc98bccc.js → public/js/groups-profile.2d5b53d784146dd1.js


BIN
public/js/groups.js


BIN
public/js/hashtag.js


BIN
public/js/home.chunk.1d1e6fe050aaaf98.js


BIN
public/js/home.chunk.491ce6f986e08bb3.js


+ 0 - 0
public/js/home.chunk.1d1e6fe050aaaf98.js.LICENSE.txt → public/js/home.chunk.491ce6f986e08bb3.js.LICENSE.txt


BIN
public/js/i18n.bundle.967974248a457514.js → public/js/i18n.bundle.873216ad86c80486.js


BIN
public/js/landing.js


BIN
public/js/manifest.js


BIN
public/js/notifications.chunk.64d01f70d8fd0ff4.js → public/js/notifications.chunk.f04bf557f846d93a.js


BIN
public/js/portfolio.js


BIN
public/js/post.chunk.13919bcfbfc2d438.js


+ 0 - 0
public/js/post.chunk.af21320999ba64af.js.LICENSE.txt → public/js/post.chunk.13919bcfbfc2d438.js.LICENSE.txt


BIN
public/js/post.chunk.af21320999ba64af.js


BIN
public/js/profile.chunk.164b255884ed6d1c.js


BIN
public/js/profile.chunk.75eb020992ddb4dd.js


BIN
public/js/profile.js


+ 1 - 0
public/js/profile.js.LICENSE.txt

@@ -0,0 +1 @@
+/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */

BIN
public/js/profile~followers.bundle.3f7b29165d67f18c.js → public/js/profile~followers.bundle.fa171ea239061d55.js


BIN
public/js/profile~following.bundle.46e6ecfbbf28d2c7.js → public/js/profile~following.bundle.7a592645bb9eb11f.js


BIN
public/js/remote_auth.js


BIN
public/js/search.js


BIN
public/js/spa.js


BIN
public/js/status.js


BIN
public/js/stories.js


BIN
public/js/story-compose.js


BIN
public/js/timeline.js


BIN
public/js/vendor.js


+ 6 - 0
public/js/vendor.js.LICENSE.txt

@@ -40,6 +40,12 @@
  * Date: 2024-04-21T07:43:05.335Z
  */
 
+/*!
+ * Glide.js v3.6.2
+ * (c) 2013-2024 Jędrzej Chałubek (https://github.com/jedrzejchalubek/)
+ * Released under the MIT License.
+ */
+
 /*!
  * JavaScript Cookie v2.2.1
  * https://github.com/js-cookie/js-cookie

BIN
public/mix-manifest.json


+ 336 - 0
resources/assets/components/FullscreenCarousel.vue

@@ -0,0 +1,336 @@
+<template>
+  <div class="fullscreen-carousel">
+    <div class="glide" ref="glide">
+      <div class="glide__track" data-glide-el="track">
+        <ul class="glide__slides">
+          <li class="glide__slide" v-for="(item, index) in feed" :key="index">
+            <div class="slide-content">
+              <img :src="item.media_url" :alt="item.caption" class="slide-image" loading="lazy">
+              <div v-if="withOverlay" class="slide-overlay">
+                <p v-if="withLinks" class="slide-username"><a :href="item.account.url">{{ webfinger }}</a></p>
+                <p v-else class="slide-username">{{ webfinger }}</p>
+                <div class="d-flex gap-1">
+                    <div v-if="withLinks" class="slide-date">
+                        <a :href="item.url" target="_blank">{{ formatDate(item.created_at) }}</a>
+                    </div>
+                    <div v-else class="slide-date">{{ formatDate(item.created_at) }}</div>
+                </div>
+              </div>
+            </div>
+          </li>
+        </ul>
+      </div>
+      
+      <div class="glide__arrows" data-glide-el="controls">
+        <button class="glide__arrow glide__arrow--left fancy-arrow" data-glide-dir="<">
+          <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+            <polyline points="15 18 9 12 15 6"></polyline>
+          </svg>
+        </button>
+        <button class="glide__arrow glide__arrow--right fancy-arrow" data-glide-dir=">">
+          <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+            <polyline points="9 18 15 12 9 6"></polyline>
+          </svg>
+        </button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import Glide from '@glidejs/glide'
+
+export default {
+  props: {
+    feed: {
+      type: Array,
+      required: true
+    },
+    canLoadMore: {
+      type: Boolean,
+      default: false
+    },
+    withLinks: {
+        type: Boolean,
+        default: false
+    },
+    withOverlay: {
+        type: Boolean,
+        default: true
+    },
+    autoPlay: {
+        type: Boolean,
+        default: false
+    },
+    autoPlayInterval: {
+        type: Number,
+        default: () => { return 5000; }
+    }
+  },
+  
+  data() {
+    return {
+      glideInstance: null
+    }
+  },
+  
+  mounted() {
+    this.initGlide()
+  },
+
+  computed: {
+    webfinger: {
+        get() {
+            if(this.feed && this.feed.length) {
+                const account = this.feed[0].account
+                const domain = new URL(account.url).host
+                return `@${account.username}@${domain}`
+            }
+            return ""
+        }
+    }
+  },
+  
+    methods: {
+        initGlide() {
+            this.glideInstance = new Glide(this.$refs.glide, {
+                type: 'carousel',
+                startAt: 0,
+                perView: 1,
+                gap: 0,
+                hoverpause: false,
+                autoplay: this.autoPlay ? this.autoPlayInterval : false,
+                keyboard: true
+            })
+
+            this.glideInstance.on('run.after', this.checkForPagination)
+            this.glideInstance.mount()
+        },
+    
+        checkForPagination() {
+            const currentIndex = this.glideInstance.index
+            if (currentIndex === this.feed.length - 1 && this.canLoadMore) {
+                this.$emit('load-more')
+            }
+        },
+    
+    loadMore() {
+      this.$emit('load-more')
+    },
+
+    formatDate(dateInput, locale = navigator.language) {
+        let date;
+
+        if (typeof dateInput === 'string') {
+            date = new Date(dateInput);
+            if (isNaN(date.getTime())) {
+                throw new Error('Invalid date string. Please provide a valid ISO 8601 format.');
+            }
+        } else if (dateInput instanceof Date) {
+            date = dateInput;
+        } else {
+            throw new Error('Invalid input. Please provide a Date object or an ISO 8601 string.');
+        }
+
+        const options = {
+            year: 'numeric',
+            month: 'long',
+            day: 'numeric',
+            hour: 'numeric',
+            minute: 'numeric',
+            hour12: true
+        };
+
+        return new Intl.DateTimeFormat(locale, options).format(date);
+    },
+    
+    updateGlide() {
+      this.$nextTick(() => {
+        if (this.glideInstance) {
+          this.glideInstance.update()
+        }
+      })
+    }
+  },
+  
+  watch: {
+    feed() {
+      this.updateGlide()
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.fullscreen-carousel {
+  height: 100dvh;
+  width: 100dvw;
+  position: relative;
+  overflow: hidden;
+  z-index: 2;
+  background: #000;
+}
+
+.glide, .glide__track, .glide__slides, .glide__slide {
+  height: 100%;
+}
+
+.slide-content {
+  position: relative;
+  height: 100%;
+  width: 100%;
+}
+
+.slide-image {
+  object-fit: contain;
+  width: 100%;
+  height: 100%;
+}
+
+.slide-overlay {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  background: rgba(0, 0, 0, 0.5);
+  color: white;
+  padding: 8px 20px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  gap: 1rem;
+}
+
+.gap-1 {
+    gap: 2rem;
+}
+
+.slide-image {
+    .slide-overlay {
+        &:not(:hover) {
+            height: 0;
+            opacity: 0;
+            transform: height 1s ease;
+        }
+    }
+}
+
+.slide-username {
+  margin: 0;
+  user-select: all;
+  font-size: 14px;
+
+  a {
+    color: white;
+    font-weight: 500;
+  }
+}
+
+.slide-caption {
+  margin: 0;
+  font-size: 14px;
+}
+
+.slide-date {
+  margin: 0;
+  font-size: 14px;
+
+    a {
+        color: white;
+        font-weight: bold;
+        text-decoration: none;
+    }
+}
+
+.glide__arrow {
+  position: absolute;
+  top: 50%;
+  transform: translateY(-50%);
+  background: rgba(255, 255, 255, 0.5);
+  border: none;
+  font-size: 24px;
+  padding: 10px;
+  cursor: pointer;
+}
+
+.fancy-arrow {
+  position: absolute;
+  top: 50%;
+  transform: translateY(-50%);
+  background: rgba(255, 255, 255, 0.2);
+  border: none;
+  border-radius: 50%;
+  width: 50px;
+  height: 50px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  cursor: pointer;
+  transition: all 0.3s ease;
+  overflow: hidden;
+}
+
+.fancy-arrow:hover {
+  background: rgba(255, 255, 255, 0.4);
+  box-shadow: 0 0 15px rgba(255, 255, 255, 0.5);
+}
+
+.fancy-arrow:focus {
+  outline: none;
+}
+
+.fancy-arrow svg {
+  width: 24px;
+  height: 24px;
+  color: white;
+  transition: all 0.3s ease;
+}
+
+.fancy-arrow:hover svg {
+  transform: scale(1.2);
+}
+
+.glide__arrow--left {
+  left: 20px;
+}
+
+.glide__arrow--right {
+  right: 20px;
+}
+
+@keyframes pulse {
+  0% {
+    transform: translateY(-50%) scale(1);
+  }
+  50% {
+    transform: translateY(-50%) scale(1.05);
+  }
+  100% {
+    transform: translateY(-50%) scale(1);
+  }
+}
+
+.fancy-arrow:active {
+  animation: pulse 0.3s ease-in-out;
+}
+
+@media (max-width: 768px) {
+  .fancy-arrow {
+    width: 40px;
+    height: 40px;
+  }
+
+  .fancy-arrow svg {
+    width: 20px;
+    height: 20px;
+  }
+
+  .glide__arrow--left {
+    left: 10px;
+  }
+
+  .glide__arrow--right {
+    right: 10px;
+  }
+}
+</style>

+ 197 - 0
resources/assets/components/ProfileCarousel.vue

@@ -0,0 +1,197 @@
+<template>
+    <div class="profile-carousel-component">
+        <template v-if="showSplash">
+            <SplashScreen />
+        </template>
+
+        <template v-else>
+            <template v-if="emptyFeed">
+                <div class="bg-dark d-flex justify-content-center align-items-center w-100 h-100">
+                    <div>
+                        <h2 class="text-light">Oops! This account hasn't posted yet or is private.</h2>
+                        <a href="/" class="font-weight-bold text-muted">Go back home</a>
+                    </div>
+                </div>
+            </template>
+
+            <template v-else>
+                <FullscreenCarousel
+                    :feed="feed"
+                    :withLinks="withLinks"
+                    :withOverlay="withOverlay"
+                    :autoPlay="autoPlay"
+                    :autoPlayInterval="autoPlayInterval"
+                    :canLoadMore="hasMoreData"
+                    @load-more="loadMoreData"
+                  />
+            </template>
+        </template>
+    </div>
+</template>
+
+<script>
+    import SplashScreen from './SplashScreen.vue';
+    import FullscreenCarousel from './FullscreenCarousel.vue'
+
+    export default {
+        props: ['profile-id'],
+
+        components: {
+            SplashScreen,
+            FullscreenCarousel
+        },
+
+        data() {
+            return {
+                showSplash: true,
+                profile: {},
+                feed: [],
+                emptyFeed: false,
+                hasMoreData: false,
+                withLinks: true,
+                withOverlay: true,
+                autoPlay: false,
+                autoPlayInterval: 5000,
+                maxId: null
+            }
+        },
+
+        mounted() {
+            const url = new URL(window.location.href);
+            const params = url.searchParams;
+            if(params.has('linkless') == true) {
+                this.withLinks = false;
+            }
+
+            if(params.has('clean') == true) {
+                this.withOverlay = false;
+            }
+
+            if(params.has('interval') == true) {
+                const val = parseInt(params.get('interval'));
+                const valid = this.validateIntegerRange(val, { min: 1000, max: 30000 })
+                if(valid) {
+                    this.autoPlayInterval = val;
+                }
+            }
+
+            if(params.has('autoplay') == true) {
+                this.autoPlay = true;
+
+            }
+            this.init();
+        },
+
+        methods: {
+            async init() {
+                await axios.get(`/api/pixelfed/v1/accounts/${this.profileId}/statuses?media_type=photo&limit=10`)
+                .then(res => {
+                    if(!res || !res.data || !res.data.length) {
+                        this.emptyFeed = true;
+                        return;
+                    }
+
+                    this.maxId = this.arrayMinId(res.data);
+                    const posts = res.data.flatMap(post =>
+                      post.media_attachments.filter(media => {
+                        return ['image/jpeg','image/png', 'image/jpg', 'image/webp'].includes(media.mime)
+                      }).map(media => ({
+                        media_url: media.url,
+                        id: post.id,
+                        caption: post.content_text,
+                        created_at: post.created_at,
+                        url: post.url,
+                        account: {
+                            username: post.account.username,
+                            url: post.account.url
+                        }
+                      }))
+                    );
+                    this.feed = posts;
+                    this.hasMoreData = res.data.length === 10;
+                    setTimeout(() => {
+                        this.showSplash = false;
+                    }, 3000);
+                })
+            },
+
+            async fetchMore() {
+                await axios.get(`/api/pixelfed/v1/accounts/${this.profileId}/statuses?media_type=photo&limit=10&max_id=${this.maxId}`)
+                .then(res => {
+                    this.maxId = this.arrayMinId(res.data);
+                    const posts = res.data.flatMap(post =>
+                      post.media_attachments.filter(media => {
+                        return ['image/jpeg','image/png', 'image/jpg', 'image/webp'].includes(media.mime)
+                      }).map(media => ({
+                        media_url: media.url,
+                        id: post.id,
+                        caption: post.content_text,
+                        created_at: post.created_at,
+                        url: post.url,
+                        account: {
+                            username: post.account.username,
+                            url: post.account.url
+                        }
+                      }))
+                    );
+                    this.feed.push(...posts);
+                    this.hasMoreData = res.data.length === 10;
+                })
+            },
+
+            arrayMinId(arr) {
+                if (arr.length === 0) return null;
+                let smallest = BigInt(arr[0].id);
+                for (let i = 1; i < arr.length; i++) {
+                    const current = BigInt(arr[i].id);
+                    if (current < smallest) {
+                        smallest = current;
+                    }
+                }
+                return smallest.toString();
+            },
+
+            loadMoreData() {
+                this.fetchMore();
+            },
+
+            validateIntegerRange(value, options = {}) {
+                if (typeof value !== 'number' || !Number.isInteger(value)) {
+                    return false;
+                }
+
+                const {
+                    min = Number.MIN_SAFE_INTEGER,
+                    max = Number.MAX_SAFE_INTEGER,
+                    inclusiveMin = true,
+                    inclusiveMax = true
+                } = options;
+
+                if (min !== undefined && !Number.isInteger(min)) {
+                    return false;
+                }
+                if (max !== undefined && !Number.isInteger(max)) {
+                    return false;
+                }
+                if (min > max) {
+                    return false;
+                }
+
+                const aboveMin = inclusiveMin ? value >= min : value > min;
+                const belowMax = inclusiveMax ? value <= max : value < max;
+
+                return aboveMin && belowMax;
+            }
+        }
+    }
+</script>
+
+<style type="text/css">
+    .profile-carousel-component {
+        display: block;
+        width: 100dvw;
+        height: 100dvh;
+        z-index: 2;
+        background: #000;
+    }
+</style>

+ 46 - 0
resources/assets/components/SplashScreen.vue

@@ -0,0 +1,46 @@
+<template>
+  <div class="splash-screen" :class="{ 'fade-out': fadeOut }">
+    <img src="/img/pixelfed-icon-white.svg" alt="Pixelfed Logo" class="logo">
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      fadeOut: false
+    }
+  },
+  mounted() {
+    setTimeout(() => {
+      this.fadeOut = true
+    }, 2000)
+  }
+}
+</script>
+
+<style scoped>
+.splash-screen {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background-color: black;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  z-index: 9999;
+  transition: opacity 1s ease-out;
+}
+
+.logo {
+  max-width: 200px;
+  max-height: 200px;
+}
+
+.fade-out {
+  opacity: 0;
+  pointer-events: none;
+}
+</style>

+ 5 - 0
resources/assets/js/profile.js

@@ -28,6 +28,11 @@ Vue.component(
     require('./components/PostMenu.vue').default
 );
 
+Vue.component(
+    'profile-carousel',
+    require('./../components/ProfileCarousel.vue').default
+);
+
 Vue.component(
     'profile',
     require('./components/Profile.vue').default

+ 2 - 0
resources/assets/sass/profile.scss

@@ -0,0 +1,2 @@
+@import "node_modules/@glidejs/glide/src/assets/sass/glide.core.scss";
+@import "node_modules/@glidejs/glide/src/assets/sass/glide.theme.scss";

+ 42 - 0
resources/views/profile/show_carousel.blade.php

@@ -0,0 +1,42 @@
+@extends('layouts.blank', [
+    'title' => $profile->name . ' (@' . $acct . ') - Pixelfed',
+    'ogTitle' => $profile->name . ' (@' . $acct . ')',
+    'ogType' => 'profile'
+])
+
+@php
+$acct = $profile->username . '@' . config('pixelfed.domain.app');
+$metaDescription = \App\Services\AccountService::getMetaDescription($profile->id);
+@endphp
+
+@section('content')
+@if (session('error'))
+        <div class="alert alert-danger text-center font-weight-bold mb-0">
+                {{ session('error') }}
+        </div>
+@endif
+
+<profile-carousel profile-id="{{$profile->id}}" />
+
+@endsection
+
+@push('meta')<meta name="description" content="{{$metaDescription}}">
+    <meta property="og:description" content="{{$metaDescription}}">
+    <meta property="og:image" content="{{$profile->avatarUrl()}}">
+    <meta property="og:image:width" content="200">
+    <meta property="og:image:height" content="200">
+    <meta property="twitter:card" content="summary">
+    <meta property="profile:username" content="{{$acct}}">
+    <link href="{{$profile->permalink('.atom')}}" rel="alternate" title="{{$profile->username}} on Pixelfed" type="application/atom+xml">
+    <link href="{{$profile->permalink()}}" rel="alternate" type="application/activity+json">
+    <meta name="application-name" content="Pixelfed">
+    <meta name="generator" content="pixelfed">
+    <link href="{{ mix('css/profile.css') }}" rel="stylesheet">
+    @if($profile->website)<link href="{{$profile->website}}" rel="me" type="text/html">
+@endif
+    @if(false == $settings['crawlable'] || $profile->remote_url)<meta name="robots" content="noindex, nofollow">@endif
+@endpush
+
+@push('scripts')<script type="text/javascript" src="{{ mix('js/profile.js') }}"></script>
+<script type="text/javascript" defer>App.boot();</script>
+@endpush

+ 1 - 0
webpack.mix.js

@@ -13,6 +13,7 @@ mix.sass('resources/assets/sass/app.scss', 'public/css')
 .sass('resources/assets/sass/admin.scss', 'public/css')
 .sass('resources/assets/sass/portfolio.scss', 'public/css')
 .sass('resources/assets/sass/spa.scss', 'public/css')
+.sass('resources/assets/sass/profile.scss', 'public/css')
 .sass('resources/assets/sass/landing.scss', 'public/css').version();
 
 mix.js('resources/assets/js/app.js', 'public/js')