InstancePicker.vue 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. <template>
  2. <div>
  3. <header>
  4. <div class="navbar navbar-expand-lg navbar-light bg-transparent">
  5. <div class="container d-flex justify-content-between navbar-nav mr-auto my-4">
  6. <a href="/" class="navbar-brand d-flex align-items-center">
  7. <img src="/img/logo.svg" width="40px" class="mr-2">
  8. <strong translated class="text-uppercase font-ptsn">pixelfed</strong>
  9. </a>
  10. </div>
  11. </div>
  12. </header>
  13. <div class="container">
  14. <div class="row mb-3">
  15. <div class="col-12 py-1 border-bottom d-flex justify-content-between align-items-center">
  16. <p class="h1 font-weight-light">Instances</p>
  17. <div class="d-none d-md-block">
  18. <span class="d-inline-block mr-5 text-center">
  19. <p class="h3 font-weight-light mb-0">{{formatCount(stats.user_count)}}</p>
  20. <div class="small text-muted">USERS</div>
  21. </span>
  22. <span class="d-inline-block mr-5 text-center">
  23. <p class="h3 font-weight-light mb-0">{{formatCount(stats.post_count)}}</p>
  24. <div class="small text-muted">POSTS</div>
  25. </span>
  26. <span class="d-inline-block text-center">
  27. <p class="h3 font-weight-light mb-0">{{formatCount(stats.instance_count)}}</p>
  28. <div class="small text-muted">INSTANCES</div>
  29. </span>
  30. </div>
  31. </div>
  32. </div>
  33. <div v-if="!loaded" class="row pt-3">
  34. <div class="col-12 py-5">
  35. <div class="d-flex justify-content-center">
  36. <div class="spinner-border" role="status">
  37. <span class="sr-only">Loading...</span>
  38. </div>
  39. </div>
  40. </div>
  41. </div>
  42. <div v-else class="row pt-3">
  43. <div class="col-12 col-md-3">
  44. <p class="small text-muted font-weight-bold">Filter Results</p>
  45. <div class="custom-control custom-checkbox mb-2">
  46. <input type="checkbox" class="custom-control-input" id="customCheck0" v-model="openRegistration">
  47. <label class="custom-control-label font-weight-bold text-muted" for="customCheck0">Open Registration</label>
  48. </div>
  49. <div class="custom-control custom-checkbox mb-2">
  50. <input type="checkbox" class="custom-control-input" id="customCheck1" v-model="latestVersionOnly">
  51. <label class="custom-control-label font-weight-bold text-muted" for="customCheck1">Latest Version Only</label>
  52. </div>
  53. <div class="custom-control custom-checkbox mb-4">
  54. <input type="checkbox" class="custom-control-input" id="customCheck2" v-model="allowsVideos">
  55. <label class="custom-control-label font-weight-bold text-muted" for="customCheck2">Allows Video Uploads</label>
  56. </div>
  57. <hr>
  58. <div class="mb-4">
  59. <label for="customRange1" class="font-weight-bold text-muted small">Photo Album Limit</label>
  60. <input type="range" class="custom-range" id="customRange1" step="1" min="1" max="15" v-model="albumSizeRange">
  61. <p class="small font-weight-bold text-muted">Minimum {{albumSizeRange}} photos allowed</p>
  62. </div>
  63. <div class="mb-4">
  64. <label for="customRange1" class="font-weight-bold text-muted small">Upload Limit</label>
  65. <input type="range" class="custom-range" id="customRange1" step="5" min="5" max="40" v-model="fileSizeLimit">
  66. <p class="small font-weight-bold text-muted">Minimum {{fileSizeLimit}} MB upload limit</p>
  67. </div>
  68. <p>
  69. <button type="button" class="btn btn-outline-secondary btn-block font-weight-bold" @click.prevent="applyFilters()">Apply Filters</button>
  70. </p>
  71. </div>
  72. <div v-if="instances.length" class="col-12 col-md-6 mb-4">
  73. <div v-for="(instance, index) in instances" class="card rounded-lg bg-white shadow border-0 mb-4">
  74. <div class="card-body p-0">
  75. <div class="media">
  76. <div class="d-none d-md-flex bg-portrait instance-img flex-column justify-content-center align-items-center">
  77. <div class="text-center">
  78. <div v-for="(p, i) in instance.timeline" class="d-inline-block m-1 cursor-pointer" @click="redirect(p.url)">
  79. <div class="square" style="width:75px; height:75px;">
  80. <div class="square-content" v-bind:style="{ 'background-image': 'url(' + p.thumbnail + ')' }"></div>
  81. </div>
  82. </div>
  83. </div>
  84. </div>
  85. <div class="media-body d-flex flex-column" style="width:100%;height: 280px;">
  86. <div class="px-3 d-flex flex-grow-1 justify-content-center">
  87. <p class="h3 align-self-center font-weight-light mb-0">{{instance.domain}}</p>
  88. </div>
  89. <div class="px-3 bg-light flex-grow-0 border-top">
  90. <div class="py-3">
  91. <p class="text-muted">
  92. <span v-if="instance.nodeinfo.openRegistrations == true" class="btn btn-outline-success btn-sm py-0 font-weight-bold mr-1">open</span>
  93. <span v-if="instance.nodeinfo.metadata.config.uploader.album_limit > 1" class="btn btn-outline-secondary btn-sm py-0 font-weight-bold mr-1">albums</span>
  94. <span v-if="instance.nodeinfo.metadata.config.uploader.media_types.includes('video/mp4')" class="btn btn-outline-secondary btn-sm py-0 font-weight-bold mr-1">video</span>
  95. <span v-if="instance.nodeinfo.software.version" class="btn btn-outline-secondary btn-sm py-0 font-weight-bold mr-1">v{{instance.nodeinfo.software.version}}</span>
  96. </p>
  97. <div class="mb-0 d-flex justify-content-between align-items-center">
  98. <span>
  99. <span class="d-inline-block">
  100. <span class="d-block font-weight-bold lead mb-n2">{{formatCount(instance.user_count)}}</span>
  101. <span class="text-muted font-weight-bold small text-uppercase">Users</span>
  102. </span>
  103. <span class="d-inline-block px-3 px-md-4">
  104. <span class="d-block font-weight-bold lead mb-n2">{{formatCount(instance.post_count)}}</span>
  105. <span class="text-muted font-weight-bold small text-uppercase">Posts</span>
  106. </span>
  107. <span class="d-inline-block">
  108. <span class="d-block font-weight-bold lead mb-n2">{{formatCount(instance.nodeinfo.usage.users.activeMonth)}}</span>
  109. <span class="text-muted font-weight-bold small text-uppercase">MAU</span>
  110. </span>
  111. </span>
  112. <span>
  113. <a class="btn btn-success font-weight-bold px-4 btn-lg" :href="'/instance/' + instance.domain">View</a>
  114. </span>
  115. </div>
  116. </div>
  117. </div>
  118. </div>
  119. </div>
  120. </div>
  121. </div>
  122. </div>
  123. <div v-else class="col-12 col-md-6">
  124. <div class="card card-body p-5 text-center">
  125. <div class="font-weight-bold">No instances found. Please try again later</div>
  126. </div>
  127. </div>
  128. <div class="col-12 col-md-3">
  129. <p class="d-none justify-content-between align-items-center">
  130. <a class="btn btn-success btn-sm py-1 font-weight-bold" href="#">Prev Page</a>
  131. <a class="btn btn-success btn-sm py-1 font-weight-bold" href="#">Next Page</a>
  132. </p>
  133. <div class="card card-body mb-3 bg-transparent">
  134. <p class="text-muted font-weight-bold mb-0 text-center">{{resultCount}} Results Found</p>
  135. <hr>
  136. <p class="mb-0"><a class="btn btn-success btn-block font-weight-bold" href="https://socialhub.activitypub.rocks/t/pixelfed-instance-picker-launches-in-open-beta/412">Add Instance</a></p>
  137. </div>
  138. </div>
  139. </div>
  140. </div>
  141. </div>
  142. </template>
  143. <script type="text/javascript">
  144. export default {
  145. data() {
  146. return {
  147. page: 1,
  148. filters: [
  149. 'latestVersion',
  150. 'allowsVideos',
  151. 'adultContent'
  152. ],
  153. openRegistration: false,
  154. latestVersionOnly: false,
  155. allowsVideos: false,
  156. adultContent: false,
  157. instances: [],
  158. albumSizeRange: 3,
  159. fileSizeLimit: 15,
  160. searchFilters: [],
  161. resultCount: 0,
  162. loaded: false,
  163. pagination: {
  164. current_page: 1,
  165. next_page_url: null,
  166. prev_page_url: null
  167. },
  168. stats: {
  169. post_count: 0,
  170. user_count: 0,
  171. instance_count: 0
  172. }
  173. }
  174. },
  175. beforeMount() {
  176. let u = new URLSearchParams(window.location.search);
  177. if(u.has('page') && u.get('page') > 1) {
  178. this.page = u.get('page');
  179. }
  180. if(u.has('openRegistration') && u.get('openRegistration') == 'true') {
  181. this.openRegistration = true;
  182. }
  183. if(u.has('latestVersionOnly') && u.get('latestVersionOnly') == 'true') {
  184. this.latestVersionOnly = true;
  185. }
  186. if(u.has('allowsVideos') && u.get('allowsVideos') == 'true') {
  187. this.allowsVideos = true;
  188. }
  189. if(u.has('albumSizeRange') && u.get('albumSizeRange') !== 3) {
  190. this.albumSizeRange = u.get('albumSizeRange');
  191. }
  192. if(u.has('fileSizeLimit') && u.get('fileSizeLimit') !== 15) {
  193. this.fileSizeLimit = u.get('fileSizeLimit');
  194. }
  195. axios.get('/api/v1/instances', {
  196. params: {
  197. page: this.page,
  198. openRegistration: this.openRegistration,
  199. latestVersionOnly: this.latestVersionOnly,
  200. allowsVideos: this.allowsVideos,
  201. albumSizeRange: this.albumSizeRange,
  202. fileSizeLimit: this.fileSizeLimit
  203. }
  204. })
  205. .then(res => {
  206. this.instances = res.data.data.map(p => {
  207. p.timeline = {};
  208. return p;
  209. });
  210. this.resultCount = res.data.total;
  211. this.loaded = true;
  212. this.fetchInstancePosts();
  213. this.fetchStats();
  214. })
  215. .catch(err => {
  216. });
  217. },
  218. methods: {
  219. applyFilters() {
  220. let params = {
  221. openRegistration: this.openRegistration,
  222. latestVersionOnly: this.latestVersionOnly,
  223. allowsVideos: this.allowsVideos,
  224. albumSizeRange: this.albumSizeRange,
  225. }
  226. if(this.fileSizeLimit != 15) {
  227. params['fileSizeLimit'] = this.fileSizeLimit;
  228. }
  229. let query = Object.keys(params).filter(key => params[key]).map(key => key + '=' + params[key]).join('&');
  230. if(query) {
  231. window.location.href = '/?' + query;
  232. } else {
  233. window.location.href = '/';
  234. }
  235. },
  236. formatCount(val) {
  237. return App.util.format.count(val);
  238. },
  239. formatSize(instance) {
  240. let count = instance.nodeinfo.metadata.config.uploader.max_photo_size / 1000;
  241. return count + 'MB';
  242. },
  243. fetchInstancePosts() {
  244. let self = this;
  245. this.instances.forEach(function(i, k) {
  246. let domain = i.domain;
  247. axios.get('/api/v1/instance/' + domain + '/timeline?limit=6')
  248. .then(res => {
  249. let data = res.data;
  250. self.instances[k].timeline = data;
  251. });
  252. });
  253. },
  254. redirect(url) {
  255. window.location.href = url;
  256. },
  257. fetchStats() {
  258. axios.get('/api/v1/stats')
  259. .then(res => {
  260. this.stats = res.data;
  261. });
  262. }
  263. }
  264. }
  265. </script>