add /upload
This commit is contained in:
		
							parent
							
								
									8687361b98
								
							
						
					
					
						commit
						feb07eaf97
					
				@ -68,7 +68,7 @@ jobs:
 | 
			
		||||
          POSTGRES_PASSWORD: ${{ secrets.DB_PASS }}
 | 
			
		||||
          PGUSER: ${{ vars.DB_USER }}
 | 
			
		||||
        ports:
 | 
			
		||||
          - 5433:5432
 | 
			
		||||
          - 5432:5432
 | 
			
		||||
        options: >-
 | 
			
		||||
          --health-cmd pg_isready
 | 
			
		||||
          --health-interval 10s
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,15 @@
 | 
			
		||||
@import "bulma";
 | 
			
		||||
 | 
			
		||||
@import "variables";
 | 
			
		||||
@import "@fortawesome/fontawesome-free/scss/brands";
 | 
			
		||||
@import "@fortawesome/fontawesome-free/scss/regular";
 | 
			
		||||
@import "@fortawesome/fontawesome-free/scss/solid";
 | 
			
		||||
@import "@fortawesome/fontawesome-free/scss/fontawesome";
 | 
			
		||||
// @import "@fortawesome/fontawesome-free/scss/brands";
 | 
			
		||||
// @import "@fortawesome/fontawesome-free/scss/regular";
 | 
			
		||||
// @import "@fortawesome/fontawesome-free/scss/solid";
 | 
			
		||||
// @import "@fortawesome/fontawesome-free/scss/fontawesome";
 | 
			
		||||
 | 
			
		||||
// @import "dropzone";
 | 
			
		||||
 | 
			
		||||
// @import "@uppy/core/dist/style.min.css";
 | 
			
		||||
// @import "@uppy/dashboard/dist/style.min.css";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.is-unclickable {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										542
									
								
								apps/bright/assets/css/dropzone.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										542
									
								
								apps/bright/assets/css/dropzone.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,542 @@
 | 
			
		||||
/* dropzone.css */
 | 
			
		||||
@-webkit-keyframes passing-through {
 | 
			
		||||
  0% {
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    -webkit-transform: translateY(40px);
 | 
			
		||||
    -moz-transform: translateY(40px);
 | 
			
		||||
    -ms-transform: translateY(40px);
 | 
			
		||||
    -o-transform: translateY(40px);
 | 
			
		||||
    transform: translateY(40px)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  30%,
 | 
			
		||||
  70% {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
    -webkit-transform: translateY(0px);
 | 
			
		||||
    -moz-transform: translateY(0px);
 | 
			
		||||
    -ms-transform: translateY(0px);
 | 
			
		||||
    -o-transform: translateY(0px);
 | 
			
		||||
    transform: translateY(0px)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  100% {
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    -webkit-transform: translateY(-40px);
 | 
			
		||||
    -moz-transform: translateY(-40px);
 | 
			
		||||
    -ms-transform: translateY(-40px);
 | 
			
		||||
    -o-transform: translateY(-40px);
 | 
			
		||||
    transform: translateY(-40px)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@-moz-keyframes passing-through {
 | 
			
		||||
  0% {
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    -webkit-transform: translateY(40px);
 | 
			
		||||
    -moz-transform: translateY(40px);
 | 
			
		||||
    -ms-transform: translateY(40px);
 | 
			
		||||
    -o-transform: translateY(40px);
 | 
			
		||||
    transform: translateY(40px)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  30%,
 | 
			
		||||
  70% {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
    -webkit-transform: translateY(0px);
 | 
			
		||||
    -moz-transform: translateY(0px);
 | 
			
		||||
    -ms-transform: translateY(0px);
 | 
			
		||||
    -o-transform: translateY(0px);
 | 
			
		||||
    transform: translateY(0px)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  100% {
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    -webkit-transform: translateY(-40px);
 | 
			
		||||
    -moz-transform: translateY(-40px);
 | 
			
		||||
    -ms-transform: translateY(-40px);
 | 
			
		||||
    -o-transform: translateY(-40px);
 | 
			
		||||
    transform: translateY(-40px)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes passing-through {
 | 
			
		||||
  0% {
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    -webkit-transform: translateY(40px);
 | 
			
		||||
    -moz-transform: translateY(40px);
 | 
			
		||||
    -ms-transform: translateY(40px);
 | 
			
		||||
    -o-transform: translateY(40px);
 | 
			
		||||
    transform: translateY(40px)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  30%,
 | 
			
		||||
  70% {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
    -webkit-transform: translateY(0px);
 | 
			
		||||
    -moz-transform: translateY(0px);
 | 
			
		||||
    -ms-transform: translateY(0px);
 | 
			
		||||
    -o-transform: translateY(0px);
 | 
			
		||||
    transform: translateY(0px)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  100% {
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    -webkit-transform: translateY(-40px);
 | 
			
		||||
    -moz-transform: translateY(-40px);
 | 
			
		||||
    -ms-transform: translateY(-40px);
 | 
			
		||||
    -o-transform: translateY(-40px);
 | 
			
		||||
    transform: translateY(-40px)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@-webkit-keyframes slide-in {
 | 
			
		||||
  0% {
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    -webkit-transform: translateY(40px);
 | 
			
		||||
    -moz-transform: translateY(40px);
 | 
			
		||||
    -ms-transform: translateY(40px);
 | 
			
		||||
    -o-transform: translateY(40px);
 | 
			
		||||
    transform: translateY(40px)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  30% {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
    -webkit-transform: translateY(0px);
 | 
			
		||||
    -moz-transform: translateY(0px);
 | 
			
		||||
    -ms-transform: translateY(0px);
 | 
			
		||||
    -o-transform: translateY(0px);
 | 
			
		||||
    transform: translateY(0px)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@-moz-keyframes slide-in {
 | 
			
		||||
  0% {
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    -webkit-transform: translateY(40px);
 | 
			
		||||
    -moz-transform: translateY(40px);
 | 
			
		||||
    -ms-transform: translateY(40px);
 | 
			
		||||
    -o-transform: translateY(40px);
 | 
			
		||||
    transform: translateY(40px)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  30% {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
    -webkit-transform: translateY(0px);
 | 
			
		||||
    -moz-transform: translateY(0px);
 | 
			
		||||
    -ms-transform: translateY(0px);
 | 
			
		||||
    -o-transform: translateY(0px);
 | 
			
		||||
    transform: translateY(0px)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes slide-in {
 | 
			
		||||
  0% {
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    -webkit-transform: translateY(40px);
 | 
			
		||||
    -moz-transform: translateY(40px);
 | 
			
		||||
    -ms-transform: translateY(40px);
 | 
			
		||||
    -o-transform: translateY(40px);
 | 
			
		||||
    transform: translateY(40px)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  30% {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
    -webkit-transform: translateY(0px);
 | 
			
		||||
    -moz-transform: translateY(0px);
 | 
			
		||||
    -ms-transform: translateY(0px);
 | 
			
		||||
    -o-transform: translateY(0px);
 | 
			
		||||
    transform: translateY(0px)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@-webkit-keyframes pulse {
 | 
			
		||||
  0% {
 | 
			
		||||
    -webkit-transform: scale(1);
 | 
			
		||||
    -moz-transform: scale(1);
 | 
			
		||||
    -ms-transform: scale(1);
 | 
			
		||||
    -o-transform: scale(1);
 | 
			
		||||
    transform: scale(1)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  10% {
 | 
			
		||||
    -webkit-transform: scale(1.1);
 | 
			
		||||
    -moz-transform: scale(1.1);
 | 
			
		||||
    -ms-transform: scale(1.1);
 | 
			
		||||
    -o-transform: scale(1.1);
 | 
			
		||||
    transform: scale(1.1)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  20% {
 | 
			
		||||
    -webkit-transform: scale(1);
 | 
			
		||||
    -moz-transform: scale(1);
 | 
			
		||||
    -ms-transform: scale(1);
 | 
			
		||||
    -o-transform: scale(1);
 | 
			
		||||
    transform: scale(1)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@-moz-keyframes pulse {
 | 
			
		||||
  0% {
 | 
			
		||||
    -webkit-transform: scale(1);
 | 
			
		||||
    -moz-transform: scale(1);
 | 
			
		||||
    -ms-transform: scale(1);
 | 
			
		||||
    -o-transform: scale(1);
 | 
			
		||||
    transform: scale(1)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  10% {
 | 
			
		||||
    -webkit-transform: scale(1.1);
 | 
			
		||||
    -moz-transform: scale(1.1);
 | 
			
		||||
    -ms-transform: scale(1.1);
 | 
			
		||||
    -o-transform: scale(1.1);
 | 
			
		||||
    transform: scale(1.1)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  20% {
 | 
			
		||||
    -webkit-transform: scale(1);
 | 
			
		||||
    -moz-transform: scale(1);
 | 
			
		||||
    -ms-transform: scale(1);
 | 
			
		||||
    -o-transform: scale(1);
 | 
			
		||||
    transform: scale(1)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes pulse {
 | 
			
		||||
  0% {
 | 
			
		||||
    -webkit-transform: scale(1);
 | 
			
		||||
    -moz-transform: scale(1);
 | 
			
		||||
    -ms-transform: scale(1);
 | 
			
		||||
    -o-transform: scale(1);
 | 
			
		||||
    transform: scale(1)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  10% {
 | 
			
		||||
    -webkit-transform: scale(1.1);
 | 
			
		||||
    -moz-transform: scale(1.1);
 | 
			
		||||
    -ms-transform: scale(1.1);
 | 
			
		||||
    -o-transform: scale(1.1);
 | 
			
		||||
    transform: scale(1.1)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  20% {
 | 
			
		||||
    -webkit-transform: scale(1);
 | 
			
		||||
    -moz-transform: scale(1);
 | 
			
		||||
    -ms-transform: scale(1);
 | 
			
		||||
    -o-transform: scale(1);
 | 
			
		||||
    transform: scale(1)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone,
 | 
			
		||||
.dropzone * {
 | 
			
		||||
  box-sizing: border-box
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone {
 | 
			
		||||
  min-height: 150px;
 | 
			
		||||
  border: 2px solid rgba(0, 0, 0, .3);
 | 
			
		||||
  background: rgb(24, 26, 27);
 | 
			
		||||
  padding: 20px 20px
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone.dz-clickable {
 | 
			
		||||
  cursor: pointer
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone.dz-clickable * {
 | 
			
		||||
  cursor: default
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone.dz-clickable .dz-message,
 | 
			
		||||
.dropzone.dz-clickable .dz-message * {
 | 
			
		||||
  cursor: pointer
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone.dz-started .dz-message {
 | 
			
		||||
  display: none
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone.dz-drag-hover {
 | 
			
		||||
  border-style: solid
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone.dz-drag-hover .dz-message {
 | 
			
		||||
  opacity: .5
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-message {
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  margin: 2em 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-message .dz-button {
 | 
			
		||||
  background: none;
 | 
			
		||||
  color: inherit;
 | 
			
		||||
  border: none;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  font: inherit;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  outline: inherit
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  vertical-align: top;
 | 
			
		||||
  margin: 16px;
 | 
			
		||||
  min-height: 100px
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview:hover {
 | 
			
		||||
  z-index: 1000
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview:hover .dz-details {
 | 
			
		||||
  opacity: 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview.dz-file-preview .dz-image {
 | 
			
		||||
  border-radius: 20px;
 | 
			
		||||
  background: #999;
 | 
			
		||||
  background: linear-gradient(to bottom, #eee, #ddd)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview.dz-file-preview .dz-details {
 | 
			
		||||
  opacity: 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview.dz-image-preview {
 | 
			
		||||
  background: #fff
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview.dz-image-preview .dz-details {
 | 
			
		||||
  -webkit-transition: opacity 0.2s linear;
 | 
			
		||||
  -moz-transition: opacity 0.2s linear;
 | 
			
		||||
  -ms-transition: opacity 0.2s linear;
 | 
			
		||||
  -o-transition: opacity 0.2s linear;
 | 
			
		||||
  transition: opacity 0.2s linear
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview .dz-remove {
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  display: block;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  border: none
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview .dz-remove:hover {
 | 
			
		||||
  text-decoration: underline
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview:hover .dz-details {
 | 
			
		||||
  opacity: 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview .dz-details {
 | 
			
		||||
  z-index: 20;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  opacity: 0;
 | 
			
		||||
  font-size: 13px;
 | 
			
		||||
  min-width: 100%;
 | 
			
		||||
  max-width: 100%;
 | 
			
		||||
  padding: 2em 1em;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  color: rgba(0, 0, 0, .9);
 | 
			
		||||
  line-height: 150%
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview .dz-details .dz-size {
 | 
			
		||||
  margin-bottom: 1em;
 | 
			
		||||
  font-size: 16px
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview .dz-details .dz-filename {
 | 
			
		||||
  white-space: nowrap
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview .dz-details .dz-filename:hover span {
 | 
			
		||||
  border: 1px solid rgba(200, 200, 200, .8);
 | 
			
		||||
  background-color: rgba(255, 255, 255, .8)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview .dz-details .dz-filename:not(:hover) {
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  text-overflow: ellipsis
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview .dz-details .dz-filename:not(:hover) span {
 | 
			
		||||
  border: 1px solid transparent
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview .dz-details .dz-filename span,
 | 
			
		||||
.dropzone .dz-preview .dz-details .dz-size span {
 | 
			
		||||
  background-color: rgba(255, 255, 255, .4);
 | 
			
		||||
  padding: 0 .4em;
 | 
			
		||||
  border-radius: 3px
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview:hover .dz-image img {
 | 
			
		||||
  -webkit-transform: scale(1.05, 1.05);
 | 
			
		||||
  -moz-transform: scale(1.05, 1.05);
 | 
			
		||||
  -ms-transform: scale(1.05, 1.05);
 | 
			
		||||
  -o-transform: scale(1.05, 1.05);
 | 
			
		||||
  transform: scale(1.05, 1.05);
 | 
			
		||||
  -webkit-filter: blur(8px);
 | 
			
		||||
  filter: blur(8px)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview .dz-image {
 | 
			
		||||
  border-radius: 20px;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  width: 120px;
 | 
			
		||||
  height: 120px;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  display: block;
 | 
			
		||||
  z-index: 10
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview .dz-image img {
 | 
			
		||||
  display: block
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview.dz-success .dz-success-mark {
 | 
			
		||||
  -webkit-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
 | 
			
		||||
  -moz-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
 | 
			
		||||
  -ms-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
 | 
			
		||||
  -o-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
 | 
			
		||||
  animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview.dz-error .dz-error-mark {
 | 
			
		||||
  opacity: 1;
 | 
			
		||||
  -webkit-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
 | 
			
		||||
  -moz-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
 | 
			
		||||
  -ms-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
 | 
			
		||||
  -o-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
 | 
			
		||||
  animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview .dz-success-mark,
 | 
			
		||||
.dropzone .dz-preview .dz-error-mark {
 | 
			
		||||
  pointer-events: none;
 | 
			
		||||
  opacity: 0;
 | 
			
		||||
  z-index: 500;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  display: block;
 | 
			
		||||
  top: 50%;
 | 
			
		||||
  left: 50%;
 | 
			
		||||
  margin-left: -27px;
 | 
			
		||||
  margin-top: -27px
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview .dz-success-mark svg,
 | 
			
		||||
.dropzone .dz-preview .dz-error-mark svg {
 | 
			
		||||
  display: block;
 | 
			
		||||
  width: 54px;
 | 
			
		||||
  height: 54px
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview.dz-processing .dz-progress {
 | 
			
		||||
  opacity: 1;
 | 
			
		||||
  -webkit-transition: all 0.2s linear;
 | 
			
		||||
  -moz-transition: all 0.2s linear;
 | 
			
		||||
  -ms-transition: all 0.2s linear;
 | 
			
		||||
  -o-transition: all 0.2s linear;
 | 
			
		||||
  transition: all 0.2s linear
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview.dz-complete .dz-progress {
 | 
			
		||||
  opacity: 0;
 | 
			
		||||
  -webkit-transition: opacity 0.4s ease-in;
 | 
			
		||||
  -moz-transition: opacity 0.4s ease-in;
 | 
			
		||||
  -ms-transition: opacity 0.4s ease-in;
 | 
			
		||||
  -o-transition: opacity 0.4s ease-in;
 | 
			
		||||
  transition: opacity 0.4s ease-in
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview:not(.dz-processing) .dz-progress {
 | 
			
		||||
  -webkit-animation: pulse 6s ease infinite;
 | 
			
		||||
  -moz-animation: pulse 6s ease infinite;
 | 
			
		||||
  -ms-animation: pulse 6s ease infinite;
 | 
			
		||||
  -o-animation: pulse 6s ease infinite;
 | 
			
		||||
  animation: pulse 6s ease infinite
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview .dz-progress {
 | 
			
		||||
  opacity: 1;
 | 
			
		||||
  z-index: 1000;
 | 
			
		||||
  pointer-events: none;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  height: 16px;
 | 
			
		||||
  left: 50%;
 | 
			
		||||
  top: 50%;
 | 
			
		||||
  margin-top: -8px;
 | 
			
		||||
  width: 80px;
 | 
			
		||||
  margin-left: -40px;
 | 
			
		||||
  background: rgba(255, 255, 255, .9);
 | 
			
		||||
  -webkit-transform: scale(1);
 | 
			
		||||
  border-radius: 8px;
 | 
			
		||||
  overflow: hidden
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview .dz-progress .dz-upload {
 | 
			
		||||
  background: #333;
 | 
			
		||||
  background: linear-gradient(to bottom, #666, #444);
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
  width: 0;
 | 
			
		||||
  -webkit-transition: width 300ms ease-in-out;
 | 
			
		||||
  -moz-transition: width 300ms ease-in-out;
 | 
			
		||||
  -ms-transition: width 300ms ease-in-out;
 | 
			
		||||
  -o-transition: width 300ms ease-in-out;
 | 
			
		||||
  transition: width 300ms ease-in-out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview.dz-error .dz-error-message {
 | 
			
		||||
  display: block
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview.dz-error:hover .dz-error-message {
 | 
			
		||||
  opacity: 1;
 | 
			
		||||
  pointer-events: auto
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview .dz-error-message {
 | 
			
		||||
  pointer-events: none;
 | 
			
		||||
  z-index: 1000;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  display: block;
 | 
			
		||||
  display: none;
 | 
			
		||||
  opacity: 0;
 | 
			
		||||
  -webkit-transition: opacity 0.3s ease;
 | 
			
		||||
  -moz-transition: opacity 0.3s ease;
 | 
			
		||||
  -ms-transition: opacity 0.3s ease;
 | 
			
		||||
  -o-transition: opacity 0.3s ease;
 | 
			
		||||
  transition: opacity 0.3s ease;
 | 
			
		||||
  border-radius: 8px;
 | 
			
		||||
  font-size: 13px;
 | 
			
		||||
  top: 130px;
 | 
			
		||||
  left: -10px;
 | 
			
		||||
  width: 140px;
 | 
			
		||||
  background: #be2626;
 | 
			
		||||
  background: linear-gradient(to bottom, #be2626, #a92222);
 | 
			
		||||
  padding: .5em 1.2em;
 | 
			
		||||
  color: #454a4d #202324
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropzone .dz-preview .dz-error-message:after {
 | 
			
		||||
  content: "";
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: -6px;
 | 
			
		||||
  left: 64px;
 | 
			
		||||
  width: 0;
 | 
			
		||||
  height: 0;
 | 
			
		||||
  border-left: 6px solid transparent;
 | 
			
		||||
  border-right: 6px solid transparent;
 | 
			
		||||
  border-bottom: 6px solid #be2626
 | 
			
		||||
}
 | 
			
		||||
@ -22,15 +22,18 @@ import {Socket} from "phoenix"
 | 
			
		||||
import { LiveSocket } from "phoenix_live_view"
 | 
			
		||||
import topbar from "../vendor/topbar"
 | 
			
		||||
import Hooks from './hooks/index.js'
 | 
			
		||||
 | 
			
		||||
import Uploaders from "./uploaders.js"
 | 
			
		||||
 | 
			
		||||
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
 | 
			
		||||
let liveSocket = new LiveSocket("/live", Socket, {
 | 
			
		||||
  longPollFallbackMs: 2500,
 | 
			
		||||
  longPollFallbackMs: 3000,
 | 
			
		||||
  params: { _csrf_token: csrfToken },
 | 
			
		||||
  hooks: Hooks
 | 
			
		||||
  hooks: Hooks,
 | 
			
		||||
  uploaders: Uploaders
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// Show progress bar on live navigation and form submits
 | 
			
		||||
topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" })
 | 
			
		||||
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,9 @@
 | 
			
		||||
import VideojsHook from "./videojs_hook.js"
 | 
			
		||||
import UppyHook from './uppy_hook.js'
 | 
			
		||||
 | 
			
		||||
let Hooks = {}
 | 
			
		||||
 | 
			
		||||
Hooks.VideojsHook = VideojsHook
 | 
			
		||||
Hooks.UppyHook = UppyHook
 | 
			
		||||
 | 
			
		||||
export default Hooks
 | 
			
		||||
							
								
								
									
										139
									
								
								apps/bright/assets/js/hooks/uppy_hook.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								apps/bright/assets/js/hooks/uppy_hook.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,139 @@
 | 
			
		||||
 | 
			
		||||
import Uppy from '@uppy/core'
 | 
			
		||||
import Dashboard from '@uppy/dashboard'
 | 
			
		||||
import GoldenRetriever from '@uppy/golden-retriever'
 | 
			
		||||
import AwsS3 from '@uppy/aws-s3'
 | 
			
		||||
 | 
			
		||||
// @see https://github.com/discourse/discourse/blob/main/app/assets/javascripts/discourse/app/lib/uppy/s3-multipart.js
 | 
			
		||||
 | 
			
		||||
const UppyHook = {
 | 
			
		||||
 | 
			
		||||
  mounted() {
 | 
			
		||||
    let live = this
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    const element = live.el;
 | 
			
		||||
 | 
			
		||||
    // Get the value of the `data-endpoint` attribute
 | 
			
		||||
    // const endpoint = element.getAttribute('data-endpoint');
 | 
			
		||||
 | 
			
		||||
    new Uppy()
 | 
			
		||||
      .use(Dashboard, {
 | 
			
		||||
        inline: true,
 | 
			
		||||
        target: '#uppy-dashboard',
 | 
			
		||||
        theme: 'dark',
 | 
			
		||||
        proudlyDisplayPoweredByUppy: false
 | 
			
		||||
      })
 | 
			
		||||
      .use(AwsS3, {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        signPart(file, partData) {
 | 
			
		||||
 | 
			
		||||
          return new Promise((resolve) => {
 | 
			
		||||
            live.pushEvent('sign_part', partData, (reply, ref) => {
 | 
			
		||||
              // console.log(`We got a response from sign_part pushEvent. reply=${JSON.stringify(reply)}`)
 | 
			
		||||
              let { url, headers } = reply
 | 
			
		||||
              // console.log(`got url=${url} and headers=${headers} from reply`)
 | 
			
		||||
              resolve({
 | 
			
		||||
                url,
 | 
			
		||||
                headers
 | 
			
		||||
              })
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
          })
 | 
			
		||||
        },
 | 
			
		||||
        createMultipartUpload(file) {
 | 
			
		||||
          // console.log(`createMultipartUpload with file ${JSON.stringify(file)}`)
 | 
			
		||||
          let { name, type } = file.meta
 | 
			
		||||
          let payload = { name, type }
 | 
			
		||||
          return new Promise((resolve) => {
 | 
			
		||||
            live.pushEvent('initiate_multipart_upload', payload, (reply, ref) => {
 | 
			
		||||
              // console.log(`payload=${JSON.stringify(payload)}`)
 | 
			
		||||
              // console.log(`initiate_multipart_upload pushEvent response callback.`)
 | 
			
		||||
              // console.log(`got reply=${JSON.stringify(reply)}`)
 | 
			
		||||
              // console.log(`got ref=${JSON.stringify(ref)}`)
 | 
			
		||||
 | 
			
		||||
              let output = {
 | 
			
		||||
                uploadId: reply?.upload_id,
 | 
			
		||||
                key: reply?.key
 | 
			
		||||
              }
 | 
			
		||||
              resolve(output)
 | 
			
		||||
            })
 | 
			
		||||
          })
 | 
			
		||||
        },
 | 
			
		||||
        completeMultipartUpload(file, { key, uploadId, parts }) {
 | 
			
		||||
          // console.log(`completeMultipartUpload with file ${JSON.stringify(file)}, parts=${JSON.stringify(parts)}`)
 | 
			
		||||
          // parts = parts.map((part) => Object.assign({}, part, { etag: part.etag.replace(/^"|"$/, "") } ))
 | 
			
		||||
          parts = parts.map((part) => ({
 | 
			
		||||
            etag: part.ETag.replace(/"/g, ""),
 | 
			
		||||
            part_number: part.PartNumber
 | 
			
		||||
          }));
 | 
			
		||||
          // console.log(parts)
 | 
			
		||||
 | 
			
		||||
          let payload = { key, uploadId, parts }
 | 
			
		||||
          return new Promise((resolve) => {
 | 
			
		||||
            live.pushEvent('complete_multipart_upload', payload, (reply, ref) => {
 | 
			
		||||
              // console.log(`payload=${JSON.stringify(payload)}`)
 | 
			
		||||
              // console.log(`complete_multipart_upload pushEvent response callback.`)
 | 
			
		||||
              // console.log(`got reply=${JSON.stringify(reply)}`)
 | 
			
		||||
              // console.log(`got ref=${JSON.stringify(ref)}`)
 | 
			
		||||
 | 
			
		||||
              let output = {
 | 
			
		||||
                location
 | 
			
		||||
              }
 | 
			
		||||
              resolve(output)
 | 
			
		||||
            })
 | 
			
		||||
          })
 | 
			
		||||
        },
 | 
			
		||||
        getUploadParameters(file, options) {
 | 
			
		||||
          // console.log(`getUploadParameters with file=${JSON.stringify(file)}, options=${JSON.stringify(options)}`)
 | 
			
		||||
          return new Promise((resolve) => {
 | 
			
		||||
            let payload = { type: file.type, name: file.name }
 | 
			
		||||
            live.pushEvent('get_upload_parameters', payload, (reply, ref) => {
 | 
			
		||||
              // console.log(`reply=${JSON.stringify(reply)}`)
 | 
			
		||||
              let { type, method, url } = reply
 | 
			
		||||
              let output = {
 | 
			
		||||
                type, method, url
 | 
			
		||||
              }
 | 
			
		||||
              resolve(output)
 | 
			
		||||
            })
 | 
			
		||||
          })
 | 
			
		||||
        },
 | 
			
		||||
        listParts(file, { uploadId, key }) {
 | 
			
		||||
          // console.log('pushing list_parts event')
 | 
			
		||||
          let payload = {
 | 
			
		||||
            uploadId,
 | 
			
		||||
            key
 | 
			
		||||
          }
 | 
			
		||||
          live.pushEvent('list_parts', payload, (reply) => {
 | 
			
		||||
            return reply
 | 
			
		||||
          })
 | 
			
		||||
        },
 | 
			
		||||
        shouldUseMultipart(file) {
 | 
			
		||||
          // Use multipart only for files larger than 100MiB.
 | 
			
		||||
          // return file.size > 100 * 2 ** 20;
 | 
			
		||||
          return file.size > 100 * 2;
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  beforeUpdate() { },
 | 
			
		||||
 | 
			
		||||
  updated() {
 | 
			
		||||
    // console.log("UppyHook updated");
 | 
			
		||||
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  destroyed() { },
 | 
			
		||||
 | 
			
		||||
  disconnected() { },
 | 
			
		||||
 | 
			
		||||
  reconnected() { }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default UppyHook
 | 
			
		||||
							
								
								
									
										26
									
								
								apps/bright/assets/js/uploaders.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								apps/bright/assets/js/uploaders.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,26 @@
 | 
			
		||||
let Uploaders = {}
 | 
			
		||||
 | 
			
		||||
Uploaders.S3 = function (entries, onViewError) {
 | 
			
		||||
  entries.forEach(entry => {
 | 
			
		||||
    let xhr = new XMLHttpRequest()
 | 
			
		||||
    onViewError(() => xhr.abort())
 | 
			
		||||
    xhr.onload = () => xhr.status === 200 ? entry.progress(100) : entry.error()
 | 
			
		||||
    xhr.onerror = () => entry.error()
 | 
			
		||||
 | 
			
		||||
    xhr.upload.addEventListener("progress", (event) => {
 | 
			
		||||
      if (event.lengthComputable) {
 | 
			
		||||
        let percent = Math.round((event.loaded / event.total) * 100)
 | 
			
		||||
        if (percent < 100) { entry.progress(percent) }
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    let url = entry.meta.url
 | 
			
		||||
    xhr.open("PUT", url, true)
 | 
			
		||||
    xhr.send(entry.file)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default Uploaders;
 | 
			
		||||
							
								
								
									
										430
									
								
								apps/bright/assets/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										430
									
								
								apps/bright/assets/package-lock.json
									
									
									
										generated
									
									
									
								
							@ -6,6 +6,12 @@
 | 
			
		||||
    "": {
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@fortawesome/fontawesome-free": "^6.7.2",
 | 
			
		||||
        "@mux/upchunk": "^3.5.0",
 | 
			
		||||
        "@uppy/aws-s3": "^4.2.3",
 | 
			
		||||
        "@uppy/core": "^4.4.3",
 | 
			
		||||
        "@uppy/dashboard": "^4.3.2",
 | 
			
		||||
        "@uppy/golden-retriever": "^4.1.1",
 | 
			
		||||
        "dropzone": "^6.0.0-beta.2",
 | 
			
		||||
        "hls.js": "^1.5.18",
 | 
			
		||||
        "vidstack": "^1.12.12"
 | 
			
		||||
      }
 | 
			
		||||
@ -44,12 +50,192 @@
 | 
			
		||||
        "node": ">=6"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@mux/upchunk": {
 | 
			
		||||
      "version": "3.5.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@mux/upchunk/-/upchunk-3.5.0.tgz",
 | 
			
		||||
      "integrity": "sha512-D+TtvlujlZQjh5I+vFzJ31h5E1uVpEaLdR8M3BNaCFbVLnFMZs8J/L/fYSUyVGnyHT/yDtPHn/IHKdo3G6oSjA==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "event-target-shim": "^6.0.2",
 | 
			
		||||
        "xhr": "^2.6.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@swc/helpers": {
 | 
			
		||||
      "version": "0.2.14",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.2.14.tgz",
 | 
			
		||||
      "integrity": "sha512-wpCQMhf5p5GhNg2MmGKXzUNwxe7zRiCsmqYsamez2beP7mKPCSiu+BjZcdN95yYSzO857kr0VfQewmGpS77nqA==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@transloadit/prettier-bytes": {
 | 
			
		||||
      "version": "0.3.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@transloadit/prettier-bytes/-/prettier-bytes-0.3.5.tgz",
 | 
			
		||||
      "integrity": "sha512-xF4A3d/ZyX2LJWeQZREZQw+qFX4TGQ8bGVP97OLRt6sPO6T0TNHBFTuRHOJh7RNmYOBmQ9MHxpolD9bXihpuVA==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@types/retry": {
 | 
			
		||||
      "version": "0.12.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz",
 | 
			
		||||
      "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@types/trusted-types": {
 | 
			
		||||
      "version": "2.0.7",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
 | 
			
		||||
      "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@uppy/aws-s3": {
 | 
			
		||||
      "version": "4.2.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@uppy/aws-s3/-/aws-s3-4.2.3.tgz",
 | 
			
		||||
      "integrity": "sha512-5vNgTE85DLujOXpzC6KEwJHLSi8o96v4rwZxMvDWQuikvX4sGcGflYjBCsPaVDYUCiiDXuhI8f93zfwCUEwQ/Q==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@uppy/companion-client": "^4.4.1",
 | 
			
		||||
        "@uppy/utils": "^6.1.1"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "@uppy/core": "^4.4.1"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@uppy/companion-client": {
 | 
			
		||||
      "version": "4.4.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@uppy/companion-client/-/companion-client-4.4.1.tgz",
 | 
			
		||||
      "integrity": "sha512-ardMacShsfzaIbqHEH48YlpzWZkBj1qhAj0Dvn3r31p9d0HA5xFUvAdLYrZ6ezKvZ0RcDbf0SB5qCrQMkjscXQ==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@uppy/utils": "^6.1.1",
 | 
			
		||||
        "namespace-emitter": "^2.0.1",
 | 
			
		||||
        "p-retry": "^6.1.0"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "@uppy/core": "^4.4.1"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@uppy/core": {
 | 
			
		||||
      "version": "4.4.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@uppy/core/-/core-4.4.3.tgz",
 | 
			
		||||
      "integrity": "sha512-Ma/v9+u0xYoxFcTajBpe0TUHI0Vjw2IKgB0AUNevhgFsBRgA03nL5n8Fac8TrC0QjPkYu7h0n2xf2EgzvyxAQA==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@transloadit/prettier-bytes": "^0.3.4",
 | 
			
		||||
        "@uppy/store-default": "^4.2.0",
 | 
			
		||||
        "@uppy/utils": "^6.1.2",
 | 
			
		||||
        "lodash": "^4.17.21",
 | 
			
		||||
        "mime-match": "^1.0.2",
 | 
			
		||||
        "namespace-emitter": "^2.0.1",
 | 
			
		||||
        "nanoid": "^5.0.9",
 | 
			
		||||
        "preact": "^10.5.13"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@uppy/dashboard": {
 | 
			
		||||
      "version": "4.3.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@uppy/dashboard/-/dashboard-4.3.2.tgz",
 | 
			
		||||
      "integrity": "sha512-6cikgcY/TMy+Fq/v03QI1BNocfm1kOxii3kuUaxnz1SFGeuZ/55+C7KKL7SP/IdeoVwH7KV+550HThT0uwIQEw==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@transloadit/prettier-bytes": "^0.3.4",
 | 
			
		||||
        "@uppy/informer": "^4.2.1",
 | 
			
		||||
        "@uppy/provider-views": "^4.4.2",
 | 
			
		||||
        "@uppy/status-bar": "^4.1.2",
 | 
			
		||||
        "@uppy/thumbnail-generator": "^4.1.1",
 | 
			
		||||
        "@uppy/utils": "^6.1.2",
 | 
			
		||||
        "classnames": "^2.2.6",
 | 
			
		||||
        "lodash": "^4.17.21",
 | 
			
		||||
        "memoize-one": "^6.0.0",
 | 
			
		||||
        "nanoid": "^5.0.9",
 | 
			
		||||
        "preact": "^10.5.13",
 | 
			
		||||
        "shallow-equal": "^3.0.0"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "@uppy/core": "^4.4.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@uppy/golden-retriever": {
 | 
			
		||||
      "version": "4.1.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@uppy/golden-retriever/-/golden-retriever-4.1.1.tgz",
 | 
			
		||||
      "integrity": "sha512-ZzgG2p0iS/4xAOVQjckIOO29otZpxJEaZr6aDvNvc67eW0VhRMqWQhq/1X4bULmKg2TVIW06vaECd71DucUsQw==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@uppy/utils": "^6.1.1",
 | 
			
		||||
        "lodash": "^4.17.21"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "@uppy/core": "^4.4.1"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@uppy/informer": {
 | 
			
		||||
      "version": "4.2.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@uppy/informer/-/informer-4.2.1.tgz",
 | 
			
		||||
      "integrity": "sha512-0en8Py47pl6RMDrgUfqFoF807W5kK5AKVJNT1SkTsLiGg5anmTIMuvmNG3k6LN4cn9P/rKyEHSdGcoBBUj9u7Q==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@uppy/utils": "^6.1.1",
 | 
			
		||||
        "preact": "^10.5.13"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "@uppy/core": "^4.4.1"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@uppy/provider-views": {
 | 
			
		||||
      "version": "4.4.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@uppy/provider-views/-/provider-views-4.4.2.tgz",
 | 
			
		||||
      "integrity": "sha512-YGrPJuydrksmMCjvo7Ty7/lDLNo/Y8zsOgWgWmVbXB0V5aRvqY49LeKY8HDlOXclKmn6dl5CeQFf7p46txRNGQ==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@uppy/utils": "^6.1.2",
 | 
			
		||||
        "classnames": "^2.2.6",
 | 
			
		||||
        "nanoid": "^5.0.9",
 | 
			
		||||
        "p-queue": "^8.0.0",
 | 
			
		||||
        "preact": "^10.5.13"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "@uppy/core": "^4.4.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@uppy/status-bar": {
 | 
			
		||||
      "version": "4.1.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@uppy/status-bar/-/status-bar-4.1.2.tgz",
 | 
			
		||||
      "integrity": "sha512-Z2fDXItoE940uMo3kwdDo4ZFPjTk5GY6y/C/G5+tSl6nL/IaDtWo5iVbAKHIH4s9SIRwCBgllEhxbmEPhuK7eA==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@transloadit/prettier-bytes": "^0.3.4",
 | 
			
		||||
        "@uppy/utils": "^6.1.2",
 | 
			
		||||
        "classnames": "^2.2.6",
 | 
			
		||||
        "preact": "^10.5.13"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "@uppy/core": "^4.4.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@uppy/store-default": {
 | 
			
		||||
      "version": "4.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@uppy/store-default/-/store-default-4.2.0.tgz",
 | 
			
		||||
      "integrity": "sha512-PieFVa8yTvRHIqsNKfpO/yaJw5Ae/hT7uT58ryw7gvCBY5bHrNWxH5N0XFe8PFHMpLpLn8v3UXGx9ib9QkB6+Q==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@uppy/thumbnail-generator": {
 | 
			
		||||
      "version": "4.1.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@uppy/thumbnail-generator/-/thumbnail-generator-4.1.1.tgz",
 | 
			
		||||
      "integrity": "sha512-65znkGNgVTbVte51IKOhgxOpHGSwYj9Qik2jF2ZBocMbhBY4gPkWFwqMrKQBfddA9KbUa4jVe1psxhAQTzYgiA==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@uppy/utils": "^6.1.1",
 | 
			
		||||
        "exifr": "^7.0.0"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "@uppy/core": "^4.4.1"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@uppy/utils": {
 | 
			
		||||
      "version": "6.1.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@uppy/utils/-/utils-6.1.2.tgz",
 | 
			
		||||
      "integrity": "sha512-PCrw6v51M6p3hlrlB2INmcocen4Dyjun1SobjVZRBkg4wutQE8ihZfSrH5ZE8UXFelufhtO16wlaZMi0EHk84w==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "lodash": "^4.17.21",
 | 
			
		||||
        "preact": "^10.5.13"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/acorn": {
 | 
			
		||||
      "version": "8.14.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
 | 
			
		||||
@ -62,12 +248,91 @@
 | 
			
		||||
        "node": ">=0.4.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/classnames": {
 | 
			
		||||
      "version": "2.5.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
 | 
			
		||||
      "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/dom-walk": {
 | 
			
		||||
      "version": "0.1.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
 | 
			
		||||
      "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/dropzone": {
 | 
			
		||||
      "version": "6.0.0-beta.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/dropzone/-/dropzone-6.0.0-beta.2.tgz",
 | 
			
		||||
      "integrity": "sha512-k44yLuFFhRk53M8zP71FaaNzJYIzr99SKmpbO/oZKNslDjNXQsBTdfLs+iONd0U0L94zzlFzRnFdqbLcs7h9fQ==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@swc/helpers": "^0.2.13",
 | 
			
		||||
        "just-extend": "^5.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/event-target-shim": {
 | 
			
		||||
      "version": "6.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-6.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-8q3LsZjRezbFZ2PN+uP+Q7pnHUMmAOziU2vA2OwoFaKIXxlxl38IylhSSgUorWu/rf4er67w0ikBqjBFk/pomA==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=10.13.0"
 | 
			
		||||
      },
 | 
			
		||||
      "funding": {
 | 
			
		||||
        "url": "https://github.com/sponsors/mysticatea"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/eventemitter3": {
 | 
			
		||||
      "version": "5.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/exifr": {
 | 
			
		||||
      "version": "7.1.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/exifr/-/exifr-7.1.3.tgz",
 | 
			
		||||
      "integrity": "sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/global": {
 | 
			
		||||
      "version": "4.4.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
 | 
			
		||||
      "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "min-document": "^2.19.0",
 | 
			
		||||
        "process": "^0.11.10"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/hls.js": {
 | 
			
		||||
      "version": "1.5.20",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.5.20.tgz",
 | 
			
		||||
      "integrity": "sha512-uu0VXUK52JhihhnN/MVVo1lvqNNuhoxkonqgO3IpjvQiGpJBdIXMGkofjQb/j9zvV7a1SW8U9g1FslWx/1HOiQ==",
 | 
			
		||||
      "license": "Apache-2.0"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/is-function": {
 | 
			
		||||
      "version": "1.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/is-network-error": {
 | 
			
		||||
      "version": "1.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=16"
 | 
			
		||||
      },
 | 
			
		||||
      "funding": {
 | 
			
		||||
        "url": "https://github.com/sponsors/sindresorhus"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/just-extend": {
 | 
			
		||||
      "version": "5.1.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-5.1.1.tgz",
 | 
			
		||||
      "integrity": "sha512-b+z6yF1d4EOyDgylzQo5IminlUmzSeqR1hs/bzjBNjuGras4FXq/6TrzjxfN0j+TmI0ltJzTNlqXUMCniciwKQ==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/lit-html": {
 | 
			
		||||
      "version": "2.8.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz",
 | 
			
		||||
@ -77,6 +342,12 @@
 | 
			
		||||
        "@types/trusted-types": "^2.0.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/lodash": {
 | 
			
		||||
      "version": "4.17.21",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
 | 
			
		||||
      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/media-captions": {
 | 
			
		||||
      "version": "1.0.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/media-captions/-/media-captions-1.0.4.tgz",
 | 
			
		||||
@ -86,6 +357,138 @@
 | 
			
		||||
        "node": ">=16"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/memoize-one": {
 | 
			
		||||
      "version": "6.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/mime-match": {
 | 
			
		||||
      "version": "1.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/mime-match/-/mime-match-1.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==",
 | 
			
		||||
      "license": "ISC",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "wildcard": "^1.1.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/min-document": {
 | 
			
		||||
      "version": "2.19.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz",
 | 
			
		||||
      "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "dom-walk": "^0.1.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/namespace-emitter": {
 | 
			
		||||
      "version": "2.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/namespace-emitter/-/namespace-emitter-2.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/nanoid": {
 | 
			
		||||
      "version": "5.1.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz",
 | 
			
		||||
      "integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==",
 | 
			
		||||
      "funding": [
 | 
			
		||||
        {
 | 
			
		||||
          "type": "github",
 | 
			
		||||
          "url": "https://github.com/sponsors/ai"
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "bin": {
 | 
			
		||||
        "nanoid": "bin/nanoid.js"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": "^18 || >=20"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/p-queue": {
 | 
			
		||||
      "version": "8.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-8.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-mxLDbbGIBEXTJL0zEx8JIylaj3xQ7Z/7eEVjcF9fJX4DBiH9oqe+oahYnlKKxm0Ci9TlWTyhSHgygxMxjIB2jw==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "eventemitter3": "^5.0.1",
 | 
			
		||||
        "p-timeout": "^6.1.2"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      },
 | 
			
		||||
      "funding": {
 | 
			
		||||
        "url": "https://github.com/sponsors/sindresorhus"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/p-retry": {
 | 
			
		||||
      "version": "6.2.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz",
 | 
			
		||||
      "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@types/retry": "0.12.2",
 | 
			
		||||
        "is-network-error": "^1.0.0",
 | 
			
		||||
        "retry": "^0.13.1"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=16.17"
 | 
			
		||||
      },
 | 
			
		||||
      "funding": {
 | 
			
		||||
        "url": "https://github.com/sponsors/sindresorhus"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/p-timeout": {
 | 
			
		||||
      "version": "6.1.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz",
 | 
			
		||||
      "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=14.16"
 | 
			
		||||
      },
 | 
			
		||||
      "funding": {
 | 
			
		||||
        "url": "https://github.com/sponsors/sindresorhus"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/parse-headers": {
 | 
			
		||||
      "version": "2.0.6",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.6.tgz",
 | 
			
		||||
      "integrity": "sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/preact": {
 | 
			
		||||
      "version": "10.26.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/preact/-/preact-10.26.4.tgz",
 | 
			
		||||
      "integrity": "sha512-KJhO7LBFTjP71d83trW+Ilnjbo+ySsaAgCfXOXUlmGzJ4ygYPWmysm77yg4emwfmoz3b22yvH5IsVFHbhUaH5w==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "funding": {
 | 
			
		||||
        "type": "opencollective",
 | 
			
		||||
        "url": "https://opencollective.com/preact"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/process": {
 | 
			
		||||
      "version": "0.11.10",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
 | 
			
		||||
      "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">= 0.6.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/retry": {
 | 
			
		||||
      "version": "0.13.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
 | 
			
		||||
      "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">= 4"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/shallow-equal": {
 | 
			
		||||
      "version": "3.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-3.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-pfVOw8QZIXpMbhBWvzBISicvToTiM5WBF1EeAUZDDSb5Dt29yl4AYbyywbJFSEsRUMr7gJaxqCdr4L3tQf9wVg==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/unplugin": {
 | 
			
		||||
      "version": "1.16.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz",
 | 
			
		||||
@ -119,6 +522,33 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
 | 
			
		||||
      "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/wildcard": {
 | 
			
		||||
      "version": "1.1.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-1.1.2.tgz",
 | 
			
		||||
      "integrity": "sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/xhr": {
 | 
			
		||||
      "version": "2.6.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz",
 | 
			
		||||
      "integrity": "sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "global": "~4.4.0",
 | 
			
		||||
        "is-function": "^1.0.1",
 | 
			
		||||
        "parse-headers": "^2.0.0",
 | 
			
		||||
        "xtend": "^4.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/xtend": {
 | 
			
		||||
      "version": "4.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=0.4"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,12 @@
 | 
			
		||||
{
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@fortawesome/fontawesome-free": "^6.7.2",
 | 
			
		||||
    "@mux/upchunk": "^3.5.0",
 | 
			
		||||
    "@uppy/aws-s3": "^4.2.3",
 | 
			
		||||
    "@uppy/core": "^4.4.3",
 | 
			
		||||
    "@uppy/dashboard": "^4.3.2",
 | 
			
		||||
    "@uppy/golden-retriever": "^4.1.1",
 | 
			
		||||
    "dropzone": "^6.0.0-beta.2",
 | 
			
		||||
    "hls.js": "^1.5.18",
 | 
			
		||||
    "vidstack": "^1.12.12"
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -71,7 +71,7 @@ config :bright, Bright.Mailer, adapter: Swoosh.Adapters.Local
 | 
			
		||||
 | 
			
		||||
# Configure esbuild (the version is required)
 | 
			
		||||
config :esbuild,
 | 
			
		||||
  version: "0.17.11",
 | 
			
		||||
  version: "0.25.1",
 | 
			
		||||
  bright: [
 | 
			
		||||
    args:
 | 
			
		||||
      ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
 | 
			
		||||
 | 
			
		||||
@ -5,12 +5,10 @@ defmodule Bright.ObanWorkers.ProcessPosts do
 | 
			
		||||
 | 
			
		||||
  use Oban.Worker, queue: :default, max_attempts: 3
 | 
			
		||||
 | 
			
		||||
  alias Bright.Vtubers.Vtuber
 | 
			
		||||
  alias Bright.Repo
 | 
			
		||||
  alias Bright.Socials.XPost
 | 
			
		||||
  alias Bright.Streams.Stream
 | 
			
		||||
  alias Bright.Platforms.Platform
 | 
			
		||||
  alias Bright.Platforms
 | 
			
		||||
  import Ecto.Query
 | 
			
		||||
 | 
			
		||||
  require Logger
 | 
			
		||||
@ -26,28 +24,30 @@ defmodule Bright.ObanWorkers.ProcessPosts do
 | 
			
		||||
 | 
			
		||||
    known_platforms = Platform |> Repo.all() |> Repo.preload(:platform_aliases)
 | 
			
		||||
 | 
			
		||||
    {num, nil} =
 | 
			
		||||
      XPost.get_unprocessed_posts()
 | 
			
		||||
      |> then(fn posts ->
 | 
			
		||||
    posts = XPost.get_unprocessed_posts()
 | 
			
		||||
    num = length(posts)
 | 
			
		||||
 | 
			
		||||
    if posts == [] do
 | 
			
		||||
      Logger.info("No unprocessed posts found")
 | 
			
		||||
    else
 | 
			
		||||
          Logger.debug("#{length(posts)} unprocessed posts found.")
 | 
			
		||||
      Logger.debug("#{num} unprocessed posts found.")
 | 
			
		||||
      Enum.each(posts, &process_post(&1, known_platforms))
 | 
			
		||||
      mark_posts_as_processed(posts)
 | 
			
		||||
    end
 | 
			
		||||
      end)
 | 
			
		||||
 | 
			
		||||
    {:ok, num}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def process_post(post, known_platforms) do
 | 
			
		||||
    with %{is_nsfw_live_announcement: is_live, platforms_mentioned: platforms} <-
 | 
			
		||||
           XPost.parse(post, known_platforms),
 | 
			
		||||
         {:ok, _stream} <- create_stream(post, platforms) do
 | 
			
		||||
      :ok
 | 
			
		||||
    else
 | 
			
		||||
      idk ->
 | 
			
		||||
    case XPost.parse(post, known_platforms) do
 | 
			
		||||
      %{is_nsfw_live_announcement: true, platforms_mentioned: platforms_mentioned} ->
 | 
			
		||||
        create_stream(post, platforms_mentioned)
 | 
			
		||||
 | 
			
		||||
      # %{is_nsfw_live_announcement: false, platforms_mentioned: _} ->
 | 
			
		||||
      #   Logger.warning("is_nsfw_live_announcement was false for post with raw=#{post.raw}")
 | 
			
		||||
      #   :ok
 | 
			
		||||
 | 
			
		||||
      _ ->
 | 
			
		||||
        Logger.debug(
 | 
			
		||||
          "process_post did not find a nsfw live announcement. post=#{inspect(post)} known_platforms=#{inspect(known_platforms)}"
 | 
			
		||||
        )
 | 
			
		||||
@ -64,9 +64,7 @@ defmodule Bright.ObanWorkers.ProcessPosts do
 | 
			
		||||
    date = post |> Map.get(:date)
 | 
			
		||||
    title = "#{post.vtuber.display_name} #{date}"
 | 
			
		||||
 | 
			
		||||
    Logger.debug(
 | 
			
		||||
      "WE ARE CREATING A Stream with platforms=#{inspect(platforms)} title=#{inspect(title)} with date=#{inspect(date)} post=#{inspect(post)}"
 | 
			
		||||
    )
 | 
			
		||||
    Logger.warning("WE ARE CREATING A Stream with post.raw=#{inspect(post.raw)}")
 | 
			
		||||
 | 
			
		||||
    changeset =
 | 
			
		||||
      %Stream{}
 | 
			
		||||
@ -80,7 +78,7 @@ defmodule Bright.ObanWorkers.ProcessPosts do
 | 
			
		||||
        Logger.info("Created stream: #{inspect(stream)}")
 | 
			
		||||
 | 
			
		||||
      {:error, %Ecto.Changeset{errors: [date: {"has already been taken", _}]}} ->
 | 
			
		||||
        Logger.warn("Stream #{title} already exists. Skipping...")
 | 
			
		||||
        Logger.warning("Stream #{title} already exists. Skipping...")
 | 
			
		||||
 | 
			
		||||
      {:error, changeset} ->
 | 
			
		||||
        Logger.error(
 | 
			
		||||
 | 
			
		||||
@ -109,26 +109,6 @@ defmodule Bright.Platforms do
 | 
			
		||||
    Platform.changeset(platform, attrs)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Creates a platform alias.
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
 | 
			
		||||
      iex> create_platform_alias(%{field: value})
 | 
			
		||||
      {:ok, %PlatformAlias{}}
 | 
			
		||||
 | 
			
		||||
      iex> create_platform(%{field: bad_value})
 | 
			
		||||
      {:error, %Ecto.Changeset{}}
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  def create_platform_alias(attrs \\ %{}) do
 | 
			
		||||
    IO.inspect(attrs, label: "PlatformAlias Attributes Before Insert")
 | 
			
		||||
 | 
			
		||||
    %PlatformAlias{}
 | 
			
		||||
    |> PlatformAlias.changeset(attrs)
 | 
			
		||||
    |> Repo.insert()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def match_platform?(a, b) do
 | 
			
		||||
    host_a = URI.parse(a.url).host
 | 
			
		||||
    host_b = URI.parse(b.url).host
 | 
			
		||||
@ -147,4 +127,119 @@ defmodule Bright.Platforms do
 | 
			
		||||
    Logger.debug("contains_platform? a=#{inspect(a)} b=#{inspect(b)}")
 | 
			
		||||
    Enum.any?(a, fn plat -> Enum.any?(b, &match_platform?(plat, &1)) end)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  alias Bright.Platforms.PlatformAlias
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Returns the list of platform_aliases.
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
 | 
			
		||||
      iex> list_platform_aliases()
 | 
			
		||||
      [%PlatformAlias{}, ...]
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  def list_platform_aliases do
 | 
			
		||||
    PlatformAlias
 | 
			
		||||
    |> Repo.all()
 | 
			
		||||
    |> Repo.preload([:platform])
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # def list_streams do
 | 
			
		||||
  #   Stream
 | 
			
		||||
  #   |> Repo.all()
 | 
			
		||||
  #   |> Repo.preload([:tags, :vods, :vtubers, :platforms, :x_post])
 | 
			
		||||
  # end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Gets a single platform_alias.
 | 
			
		||||
 | 
			
		||||
  Raises `Ecto.NoResultsError` if the Platform alias does not exist.
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
 | 
			
		||||
      iex> get_platform_alias!(123)
 | 
			
		||||
      %PlatformAlias{}
 | 
			
		||||
 | 
			
		||||
      iex> get_platform_alias!(456)
 | 
			
		||||
      ** (Ecto.NoResultsError)
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  def get_platform_alias!(id) do
 | 
			
		||||
    PlatformAlias
 | 
			
		||||
    |> Repo.get!(id)
 | 
			
		||||
    |> Repo.preload([:platform])
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # reference
 | 
			
		||||
  # def get_stream!(id) do
 | 
			
		||||
  #   Stream
 | 
			
		||||
  #   |> Repo.get!(id)
 | 
			
		||||
  #   |> Repo.preload([:tags, :vods, :vtubers, :platforms, :x_post])
 | 
			
		||||
  # end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Creates a platform_alias.
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
 | 
			
		||||
      iex> create_platform_alias(%{field: value})
 | 
			
		||||
      {:ok, %PlatformAlias{}}
 | 
			
		||||
 | 
			
		||||
      iex> create_platform_alias(%{field: bad_value})
 | 
			
		||||
      {:error, %Ecto.Changeset{}}
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  def create_platform_alias(attrs \\ %{}) do
 | 
			
		||||
    %PlatformAlias{}
 | 
			
		||||
    |> PlatformAlias.changeset(attrs)
 | 
			
		||||
    |> Repo.insert()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Updates a platform_alias.
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
 | 
			
		||||
      iex> update_platform_alias(platform_alias, %{field: new_value})
 | 
			
		||||
      {:ok, %PlatformAlias{}}
 | 
			
		||||
 | 
			
		||||
      iex> update_platform_alias(platform_alias, %{field: bad_value})
 | 
			
		||||
      {:error, %Ecto.Changeset{}}
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  def update_platform_alias(%PlatformAlias{} = platform_alias, attrs) do
 | 
			
		||||
    platform_alias
 | 
			
		||||
    |> PlatformAlias.changeset(attrs)
 | 
			
		||||
    |> Repo.update()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Deletes a platform_alias.
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
 | 
			
		||||
      iex> delete_platform_alias(platform_alias)
 | 
			
		||||
      {:ok, %PlatformAlias{}}
 | 
			
		||||
 | 
			
		||||
      iex> delete_platform_alias(platform_alias)
 | 
			
		||||
      {:error, %Ecto.Changeset{}}
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  def delete_platform_alias(%PlatformAlias{} = platform_alias) do
 | 
			
		||||
    Repo.delete(platform_alias)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Returns an `%Ecto.Changeset{}` for tracking platform_alias changes.
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
 | 
			
		||||
      iex> change_platform_alias(platform_alias)
 | 
			
		||||
      %Ecto.Changeset{data: %PlatformAlias{}}
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  def change_platform_alias(%PlatformAlias{} = platform_alias, attrs \\ %{}) do
 | 
			
		||||
    PlatformAlias.changeset(platform_alias, attrs)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,7 @@ defmodule Bright.Socials.XPost do
 | 
			
		||||
  alias Bright.Socials.{XPost, RSSParser}
 | 
			
		||||
  alias Bright.Platforms.Platform
 | 
			
		||||
  alias Bright.Platforms
 | 
			
		||||
  alias Bright.Streams.Stream
 | 
			
		||||
  alias Quinn
 | 
			
		||||
  require Logger
 | 
			
		||||
 | 
			
		||||
@ -113,7 +114,7 @@ defmodule Bright.Socials.XPost do
 | 
			
		||||
  """
 | 
			
		||||
  def includes_platform?(
 | 
			
		||||
        raw_text,
 | 
			
		||||
        %Platform{url: url, platform_aliases: %Ecto.Association.NotLoaded{}} = platform
 | 
			
		||||
        %Platform{platform_aliases: %Ecto.Association.NotLoaded{}} = platform
 | 
			
		||||
      ) do
 | 
			
		||||
    platform = Repo.preload(platform, :platform_aliases)
 | 
			
		||||
 | 
			
		||||
@ -178,7 +179,7 @@ defmodule Bright.Socials.XPost do
 | 
			
		||||
    parse(post, known_platforms)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def parse(%XPost{vtuber: vtuber} = post, known_platforms) when is_list(known_platforms) do
 | 
			
		||||
  def parse(%XPost{} = post, known_platforms) when is_list(known_platforms) do
 | 
			
		||||
    is_nsfw_live_announcement = is_nsfw_live_announcement?(post, known_platforms)
 | 
			
		||||
    platforms_mentioned = get_platforms_mentioned(post, known_platforms)
 | 
			
		||||
 | 
			
		||||
@ -195,7 +196,7 @@ defmodule Bright.Socials.XPost do
 | 
			
		||||
  To be considered a NSFW live announcement, a post must satisfy all the following conditions.
 | 
			
		||||
 | 
			
		||||
    * The post is authored by the lewdtuber
 | 
			
		||||
    * The post does not contain, "VOD/i"
 | 
			
		||||
    * The post does not contain, "VOD/i" or "/post"
 | 
			
		||||
    * The post mentions a NSFW platform
 | 
			
		||||
    * The post does not mention any SFW streaming platform.
 | 
			
		||||
 | 
			
		||||
@ -216,7 +217,7 @@ defmodule Bright.Socials.XPost do
 | 
			
		||||
 | 
			
		||||
    conditions = [
 | 
			
		||||
      {:is_not_rt, XPost.authored_by_vtuber?(post, vtuber)},
 | 
			
		||||
      {:is_not_vod, not String.contains?(String.downcase(post.raw), "vod")},
 | 
			
		||||
      {:is_not_vod, not String.contains?(String.downcase(post.raw), ["vod", "/post"])},
 | 
			
		||||
      {:is_nsfw_platform, Platforms.contains_platform?(nsfw_platforms, mentioned_platforms)},
 | 
			
		||||
      {:is_no_sfw_platform, not Platforms.contains_platform?(mentioned_platforms, sfw_platforms)}
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
@ -33,6 +33,7 @@ defmodule Bright.Streams do
 | 
			
		||||
  """
 | 
			
		||||
  def list_streams do
 | 
			
		||||
    Stream
 | 
			
		||||
    |> order_by(desc: :date)
 | 
			
		||||
    |> Repo.all()
 | 
			
		||||
    |> Repo.preload([:tags, :vods, :vtubers, :platforms, :x_post])
 | 
			
		||||
  end
 | 
			
		||||
@ -602,4 +603,100 @@ defmodule Bright.Streams do
 | 
			
		||||
 | 
			
		||||
    Repo.aggregate(query, :count, :id)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  alias Bright.Streams.Upload
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Returns the list of uploads.
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
 | 
			
		||||
      iex> list_uploads()
 | 
			
		||||
      [%Upload{}, ...]
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  def list_uploads do
 | 
			
		||||
    Repo.all(Upload)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Gets a single upload.
 | 
			
		||||
 | 
			
		||||
  Raises `Ecto.NoResultsError` if the Upload does not exist.
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
 | 
			
		||||
      iex> get_upload!(123)
 | 
			
		||||
      %Upload{}
 | 
			
		||||
 | 
			
		||||
      iex> get_upload!(456)
 | 
			
		||||
      ** (Ecto.NoResultsError)
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  def get_upload!(id), do: Repo.get!(Upload, id)
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Creates a upload.
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
 | 
			
		||||
      iex> create_upload(%{field: value})
 | 
			
		||||
      {:ok, %Upload{}}
 | 
			
		||||
 | 
			
		||||
      iex> create_upload(%{field: bad_value})
 | 
			
		||||
      {:error, %Ecto.Changeset{}}
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  def create_upload(attrs \\ %{}) do
 | 
			
		||||
    %Upload{}
 | 
			
		||||
    |> Upload.changeset(attrs)
 | 
			
		||||
    |> Repo.insert()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Updates a upload.
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
 | 
			
		||||
      iex> update_upload(upload, %{field: new_value})
 | 
			
		||||
      {:ok, %Upload{}}
 | 
			
		||||
 | 
			
		||||
      iex> update_upload(upload, %{field: bad_value})
 | 
			
		||||
      {:error, %Ecto.Changeset{}}
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  def update_upload(%Upload{} = upload, attrs) do
 | 
			
		||||
    upload
 | 
			
		||||
    |> Upload.changeset(attrs)
 | 
			
		||||
    |> Repo.update()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Deletes a upload.
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
 | 
			
		||||
      iex> delete_upload(upload)
 | 
			
		||||
      {:ok, %Upload{}}
 | 
			
		||||
 | 
			
		||||
      iex> delete_upload(upload)
 | 
			
		||||
      {:error, %Ecto.Changeset{}}
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  def delete_upload(%Upload{} = upload) do
 | 
			
		||||
    Repo.delete(upload)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Returns an `%Ecto.Changeset{}` for tracking upload changes.
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
 | 
			
		||||
      iex> change_upload(upload)
 | 
			
		||||
      %Ecto.Changeset{data: %Upload{}}
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  def change_upload(%Upload{} = upload, attrs \\ %{}) do
 | 
			
		||||
    Upload.changeset(upload, attrs)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										21
									
								
								apps/bright/lib/bright/streams/upload.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								apps/bright/lib/bright/streams/upload.ex
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
			
		||||
defmodule Bright.Streams.Upload do
 | 
			
		||||
  use Ecto.Schema
 | 
			
		||||
  import Ecto.Changeset
 | 
			
		||||
 | 
			
		||||
  schema "uploads" do
 | 
			
		||||
    field :size, :integer
 | 
			
		||||
    field :filename, :string
 | 
			
		||||
    field :content_type, :string
 | 
			
		||||
    field :user_id, :id
 | 
			
		||||
    field :vod, :id
 | 
			
		||||
 | 
			
		||||
    timestamps(type: :utc_datetime)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc false
 | 
			
		||||
  def changeset(upload, attrs) do
 | 
			
		||||
    upload
 | 
			
		||||
    |> cast(attrs, [:filename, :size, :content_type])
 | 
			
		||||
    |> validate_required([:filename, :size, :content_type])
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										17
									
								
								apps/bright/lib/bright/utils.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								apps/bright/lib/bright/utils.ex
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
			
		||||
defmodule Bright.Utils do
 | 
			
		||||
  @chars ~c"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Generates a random string of the given length.
 | 
			
		||||
 | 
			
		||||
  ## Parameters
 | 
			
		||||
    - length: The length of the random string to generate.
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
      iex> RandomString.generate(12)
 | 
			
		||||
      "aB3dEfG7hIjK"
 | 
			
		||||
  """
 | 
			
		||||
  def random_string(length \\ 12) do
 | 
			
		||||
    for _ <- 1..length, into: "", do: <<Enum.random(@chars)>>
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@ -85,7 +85,6 @@ defmodule BrightWeb do
 | 
			
		||||
      import Phoenix.HTML
 | 
			
		||||
      # Core UI components and translation
 | 
			
		||||
      import BrightWeb.CoreComponents
 | 
			
		||||
      import BrightWeb.SVGIcon
 | 
			
		||||
      import BrightWeb.Gettext
 | 
			
		||||
 | 
			
		||||
      # Shortcut for generating JS commands
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,6 @@ defmodule BrightWeb.CoreComponents do
 | 
			
		||||
  use Gettext, backend: BrightWeb.Gettext
 | 
			
		||||
 | 
			
		||||
  alias Phoenix.LiveView.JS
 | 
			
		||||
  import BrightWeb.SVGIcon
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Renders an external link.
 | 
			
		||||
@ -628,15 +627,11 @@ defmodule BrightWeb.CoreComponents do
 | 
			
		||||
  # attr :style, :string, default: "solid"
 | 
			
		||||
  # attr :class, :string, default: ""
 | 
			
		||||
 | 
			
		||||
  # def icon(assigns) do
 | 
			
		||||
  #   ~H"""
 | 
			
		||||
  #   <i class={"fa#{style_prefix(@style)} fa-#{@name} #{@class}"}></i>
 | 
			
		||||
  #   """
 | 
			
		||||
  # end
 | 
			
		||||
 | 
			
		||||
  defp style_prefix("solid"), do: "s"
 | 
			
		||||
  defp style_prefix("brands"), do: "b"
 | 
			
		||||
  defp style_prefix(_), do: "s"
 | 
			
		||||
  def icon(assigns) do
 | 
			
		||||
    ~H"""
 | 
			
		||||
    <span>🇪</span>
 | 
			
		||||
    """
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  ## JS Commands
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -52,6 +52,8 @@
 | 
			
		||||
      <div class="navbar-end">
 | 
			
		||||
        <div class="navbar-item">
 | 
			
		||||
          <.icon name="person_digging" type="solid" class="h-4 w-4 is-unclickable" />
 | 
			
		||||
          <.icon name="magnet" type="solid" class="h-4 w-4 is-unclickable" />
 | 
			
		||||
          <.icon name="circle-exclamation" type="solid" class="h-4 w-4 is-unclickable" />
 | 
			
		||||
          <.icon name="hammer" class="is-unclickable" />
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,122 +0,0 @@
 | 
			
		||||
defmodule BrightWeb.SVGIcon do
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  This package adds a convenient way of using svg icons with your Phoenix, Phoenix LiveView and Surface applications.
 | 
			
		||||
 | 
			
		||||
  greets https://github.com/miguel-s/ex_heroicons/blob/main/lib/heroicons.ex
 | 
			
		||||
 | 
			
		||||
  ## Usage
 | 
			
		||||
 | 
			
		||||
      <SVGIcons.icon name="chaturbate" class="h-4 w-4" />
 | 
			
		||||
 | 
			
		||||
  ## Config
 | 
			
		||||
 | 
			
		||||
  Defaults can be set in the application configuration.
 | 
			
		||||
 | 
			
		||||
      config :bright, :icons_type: "outline"
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  use Phoenix.Component
 | 
			
		||||
  alias BrightWeb.SVGIcon.Icon
 | 
			
		||||
 | 
			
		||||
  svg_icons_path = "priv/static/assets/icons"
 | 
			
		||||
 | 
			
		||||
  unless File.exists?(svg_icons_path) do
 | 
			
		||||
    raise """
 | 
			
		||||
    SVG icons not found. Expected to load them from #{svg_icons_path}.
 | 
			
		||||
    """
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  icon_paths =
 | 
			
		||||
    svg_icons_path
 | 
			
		||||
    |> Path.join("**/*.svg")
 | 
			
		||||
    |> Path.wildcard()
 | 
			
		||||
 | 
			
		||||
  icons =
 | 
			
		||||
    for icon_path <- icon_paths do
 | 
			
		||||
      @external_resource Path.relative_to_cwd(icon_path)
 | 
			
		||||
      Icon.parse!(icon_path)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
  types = icons |> Enum.map(& &1.type) |> Enum.uniq()
 | 
			
		||||
  names = icons |> Enum.map(& &1.name) |> Enum.uniq()
 | 
			
		||||
 | 
			
		||||
  default_type =
 | 
			
		||||
    case Application.compile_env(:bright, :icons_type) do
 | 
			
		||||
      nil ->
 | 
			
		||||
        "solid"
 | 
			
		||||
 | 
			
		||||
      type when is_binary(type) ->
 | 
			
		||||
        if type in types do
 | 
			
		||||
          type
 | 
			
		||||
        else
 | 
			
		||||
          raise ArgumentError,
 | 
			
		||||
                "expected default type to be one of #{inspect(types)}, got: #{inspect(type)}"
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
      type ->
 | 
			
		||||
        raise ArgumentError,
 | 
			
		||||
              "expected default type to be one of #{inspect(types)}, got: #{inspect(type)}"
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
  @names names
 | 
			
		||||
  def names, do: @names
 | 
			
		||||
 | 
			
		||||
  @types types
 | 
			
		||||
  def types, do: @types
 | 
			
		||||
 | 
			
		||||
  attr :name, :string, values: @names, required: true, doc: "the name of the icon"
 | 
			
		||||
  attr :type, :string, values: @types, default: default_type, doc: "the type of the icon"
 | 
			
		||||
  attr :class, :string, default: nil, doc: "the css classes to add to the svg container"
 | 
			
		||||
  attr :rest, :global, doc: "the arbitrary HTML attributes to add to the svg container"
 | 
			
		||||
 | 
			
		||||
  def icon(assigns) do
 | 
			
		||||
    name = assigns[:name]
 | 
			
		||||
 | 
			
		||||
    if name == nil or name not in @names do
 | 
			
		||||
      raise ArgumentError,
 | 
			
		||||
            "expected icon name to be one of #{inspect(unquote(@names))}, got: #{inspect(name)}"
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    type = assigns[:type]
 | 
			
		||||
 | 
			
		||||
    if type == nil or type not in @types do
 | 
			
		||||
      raise ArgumentError,
 | 
			
		||||
            "expected icon type to be one of #{inspect(unquote(@types))}, got: #{inspect(type)}"
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    ~H"""
 | 
			
		||||
    <span class="icon">
 | 
			
		||||
      <.svg_container focusable="false" type={@type} class={@class} {@rest}>
 | 
			
		||||
        <%= {:safe, svg_body(@name, @type)} %>
 | 
			
		||||
      </.svg_container>
 | 
			
		||||
    </span>
 | 
			
		||||
    """
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  attr :type, :string, values: @types, default: default_type, doc: "the type of the icon"
 | 
			
		||||
  attr :class, :string, default: nil, doc: "the css classes to add to the svg container"
 | 
			
		||||
  attr :rest, :global, doc: "the arbitrary HTML attributes to add to the svg container"
 | 
			
		||||
 | 
			
		||||
  slot :inner_block, required: true, doc: "the svg to render"
 | 
			
		||||
 | 
			
		||||
  defp svg_container(assigns) do
 | 
			
		||||
    ~H"""
 | 
			
		||||
    <%= render_slot(@inner_block) %>
 | 
			
		||||
    """
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp svg_viewbox(type) do
 | 
			
		||||
    case type do
 | 
			
		||||
      "micro" -> "0 0 16 16"
 | 
			
		||||
      "mini" -> "0 0 20 20"
 | 
			
		||||
      "solid" -> "0 0 24 24"
 | 
			
		||||
      "outline" -> "0 0 24 24"
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  for %Icon{name: name, type: type, file: file} <- icons do
 | 
			
		||||
    defp svg_body(unquote(name), unquote(type)) do
 | 
			
		||||
      unquote(file)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@ -1,44 +0,0 @@
 | 
			
		||||
defmodule BrightWeb.SVGIcon.Icon do
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  This module defines the data structure and functions for working with icons stored as SVG files.
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  alias __MODULE__
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Defines the SVGIcon.Icon struct.
 | 
			
		||||
 | 
			
		||||
  Its fields are:
 | 
			
		||||
 | 
			
		||||
    * `:type` - the type of the icon
 | 
			
		||||
    * `:name` - the name of the icon
 | 
			
		||||
    * `:file` - the binary content of the file
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  defstruct [:type, :name, :file]
 | 
			
		||||
 | 
			
		||||
  @type t :: %Icon{type: String.t(), name: String.t(), file: binary}
 | 
			
		||||
 | 
			
		||||
  @doc "Parses a SVG file and returns structured data"
 | 
			
		||||
  @spec parse!(String.t()) :: Icon.t()
 | 
			
		||||
  def parse!(filename) do
 | 
			
		||||
    [type, name] =
 | 
			
		||||
      filename
 | 
			
		||||
      |> Path.split()
 | 
			
		||||
      |> Enum.take(-2)
 | 
			
		||||
      |> case do
 | 
			
		||||
        ["solid", name] -> ["solid", name]
 | 
			
		||||
        ["outline", name] -> ["outline", name]
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    name = Path.rootname(name)
 | 
			
		||||
 | 
			
		||||
    file =
 | 
			
		||||
      filename
 | 
			
		||||
      |> File.read!()
 | 
			
		||||
      |> String.split("\n")
 | 
			
		||||
      |> Enum.map(&String.trim/1)
 | 
			
		||||
 | 
			
		||||
    %__MODULE__{type: type, name: name, file: file}
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@ -38,6 +38,10 @@ defmodule BrightWeb.PageController do
 | 
			
		||||
    json(conn, data)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def upload(conn, _params) do
 | 
			
		||||
    render(conn, :upload)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def redirect_test(conn, _params) do
 | 
			
		||||
    render(conn, :home)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
@ -8,8 +8,6 @@
 | 
			
		||||
    </div>
 | 
			
		||||
  </section>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  <div class="section">
 | 
			
		||||
    <p>
 | 
			
		||||
      A platform built by fans, for fans, dedicated to preserving the moments that matter in the world of R-18 VTuber live streaming. It all started with a simple need: capturing ProjektMelody's streams on Chaturbate. Chaturbate doesn’t save VODs, and sometimes we missed the magic. Other times, creators like ProjektMelody faced unnecessary de-platforming for simply being unique. We wanted to create a space where this content could endure, unshaken by the tides of censorship or fleeting platforms.
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,6 @@
 | 
			
		||||
  <p>@todo documentation</p>
 | 
			
		||||
</section>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<div class="section">
 | 
			
		||||
  <p class="title">icons test</p>
 | 
			
		||||
  <.icon name="bittorrent" type="solid" />
 | 
			
		||||
 | 
			
		||||
@ -15,9 +15,7 @@
 | 
			
		||||
        </p>
 | 
			
		||||
        <i>
 | 
			
		||||
          <p class="mt-3">
 | 
			
		||||
            <% # <.icon name="circle-exclamation" class="icon h-4 w-4" /> %>
 | 
			
		||||
            
 | 
			
		||||
            (Yeah, we’re still testing.) check back each week for updates.
 | 
			
		||||
            <% # <.icon name="circle-exclamation" class="icon h-4 w-4" /> %> (Yeah, we’re still testing.) check back each week for updates.
 | 
			
		||||
          </p>
 | 
			
		||||
        </i>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,48 +0,0 @@
 | 
			
		||||
<.flash_group flash={@flash} />
 | 
			
		||||
 | 
			
		||||
<%= if @current_user do %>
 | 
			
		||||
  <main class="section">
 | 
			
		||||
    <h2 class="title is-2">Profile</h2>
 | 
			
		||||
    <p class="subtitle">{@current_user.identicon_seed}</p>
 | 
			
		||||
  </main>
 | 
			
		||||
 | 
			
		||||
  <p class="title">@deprecated</p>
 | 
			
		||||
  <section class="section">
 | 
			
		||||
    <div class="card">
 | 
			
		||||
      <div class="card-content">
 | 
			
		||||
        <div class="media">
 | 
			
		||||
          <div class="media-left">
 | 
			
		||||
            <figure class="image is-48x48">
 | 
			
		||||
              {raw(
 | 
			
		||||
                IdenticonSvg.generate(@current_user.identicon_seed, 5, :basic, 0.8, 2,
 | 
			
		||||
                  squircle_curvature: 0.8
 | 
			
		||||
                )
 | 
			
		||||
              )}
 | 
			
		||||
            </figure>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="media-content">
 | 
			
		||||
            <p class="title is-4">{@current_user.identicon_seed}</p>
 | 
			
		||||
            <p class="subtitle is-6">Github User {@current_user.github_id}</p>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="content">
 | 
			
		||||
          <p class="subtitle is-6">Futureporn User {@current_user.id}</p>
 | 
			
		||||
          <p class="subtitle is-6"><i>n</i> uploads</p>
 | 
			
		||||
          Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus nec
 | 
			
		||||
          iaculis mauris. <.link>@bulmaio</.link>. <.link href="#">#css</.link>
 | 
			
		||||
          <.link href="#">#responsive</.link>
 | 
			
		||||
          <br />
 | 
			
		||||
          <time datetime="2016-1-1">11:09 PM - 1 Jan 2016</time>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </section>
 | 
			
		||||
<% else %>
 | 
			
		||||
  <.flash_group flash={@flash} />
 | 
			
		||||
 | 
			
		||||
  <section class="section">
 | 
			
		||||
    <p>User not found in session.</p>
 | 
			
		||||
    <p>Please <.link href={~p"/auth/patreon"}>sign in</.link></p>
 | 
			
		||||
  </section>
 | 
			
		||||
<% end %>
 | 
			
		||||
@ -0,0 +1,62 @@
 | 
			
		||||
defmodule BrightWeb.PlatformAliasController do
 | 
			
		||||
  use BrightWeb, :controller
 | 
			
		||||
 | 
			
		||||
  alias Bright.Platforms
 | 
			
		||||
  alias Bright.Platforms.PlatformAlias
 | 
			
		||||
 | 
			
		||||
  def index(conn, _params) do
 | 
			
		||||
    platform_aliases = Platforms.list_platform_aliases()
 | 
			
		||||
    render(conn, :index, platform_aliases: platform_aliases)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def new(conn, _params) do
 | 
			
		||||
    changeset = Platforms.change_platform_alias(%PlatformAlias{})
 | 
			
		||||
    render(conn, :new, changeset: changeset)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def create(conn, %{"platform_alias" => platform_alias_params}) do
 | 
			
		||||
    case Platforms.create_platform_alias(platform_alias_params) do
 | 
			
		||||
      {:ok, platform_alias} ->
 | 
			
		||||
        conn
 | 
			
		||||
        |> put_flash(:info, "Platform alias created successfully.")
 | 
			
		||||
        |> redirect(to: ~p"/platform_aliases/#{platform_alias}")
 | 
			
		||||
 | 
			
		||||
      {:error, %Ecto.Changeset{} = changeset} ->
 | 
			
		||||
        render(conn, :new, changeset: changeset)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def show(conn, %{"id" => id}) do
 | 
			
		||||
    platform_alias = Platforms.get_platform_alias!(id)
 | 
			
		||||
    render(conn, :show, platform_alias: platform_alias)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def edit(conn, %{"id" => id}) do
 | 
			
		||||
    platform_alias = Platforms.get_platform_alias!(id)
 | 
			
		||||
    changeset = Platforms.change_platform_alias(platform_alias)
 | 
			
		||||
    render(conn, :edit, platform_alias: platform_alias, changeset: changeset)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def update(conn, %{"id" => id, "platform_alias" => platform_alias_params}) do
 | 
			
		||||
    platform_alias = Platforms.get_platform_alias!(id)
 | 
			
		||||
 | 
			
		||||
    case Platforms.update_platform_alias(platform_alias, platform_alias_params) do
 | 
			
		||||
      {:ok, platform_alias} ->
 | 
			
		||||
        conn
 | 
			
		||||
        |> put_flash(:info, "Platform alias updated successfully.")
 | 
			
		||||
        |> redirect(to: ~p"/platform_aliases/#{platform_alias}")
 | 
			
		||||
 | 
			
		||||
      {:error, %Ecto.Changeset{} = changeset} ->
 | 
			
		||||
        render(conn, :edit, platform_alias: platform_alias, changeset: changeset)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def delete(conn, %{"id" => id}) do
 | 
			
		||||
    platform_alias = Platforms.get_platform_alias!(id)
 | 
			
		||||
    {:ok, _platform_alias} = Platforms.delete_platform_alias(platform_alias)
 | 
			
		||||
 | 
			
		||||
    conn
 | 
			
		||||
    |> put_flash(:info, "Platform alias deleted successfully.")
 | 
			
		||||
    |> redirect(to: ~p"/platform_aliases")
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@ -0,0 +1,23 @@
 | 
			
		||||
defmodule BrightWeb.PlatformAliasHTML do
 | 
			
		||||
  use BrightWeb, :html
 | 
			
		||||
 | 
			
		||||
  embed_templates "platform_alias_html/*"
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Renders a platform_alias form.
 | 
			
		||||
  """
 | 
			
		||||
  attr :changeset, Ecto.Changeset, required: true
 | 
			
		||||
  attr :action, :string, required: true
 | 
			
		||||
 | 
			
		||||
  def platform_alias_form(assigns)
 | 
			
		||||
 | 
			
		||||
  def platform_opts(changeset) do
 | 
			
		||||
    existing_ids =
 | 
			
		||||
      changeset
 | 
			
		||||
      |> Ecto.Changeset.get_change(:vtubers, [])
 | 
			
		||||
      |> Enum.map(& &1.data.id)
 | 
			
		||||
 | 
			
		||||
    for platform <- Bright.Platforms.list_platforms(),
 | 
			
		||||
        do: [key: platform.name, value: platform.id, selected: platform.id in existing_ids]
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@ -0,0 +1,8 @@
 | 
			
		||||
<.header>
 | 
			
		||||
  Edit Platform alias {@platform_alias.id}
 | 
			
		||||
  <:subtitle>Use this form to manage platform_alias records in your database.</:subtitle>
 | 
			
		||||
</.header>
 | 
			
		||||
 | 
			
		||||
<.platform_alias_form changeset={@changeset} action={~p"/platform_aliases/#{@platform_alias}"} />
 | 
			
		||||
 | 
			
		||||
<.back navigate={~p"/platform_aliases"}>Back to platform_aliases</.back>
 | 
			
		||||
@ -0,0 +1,33 @@
 | 
			
		||||
<.header>
 | 
			
		||||
  Listing Platform aliases
 | 
			
		||||
  <:actions>
 | 
			
		||||
    <.link href={~p"/platform_aliases/new"}>
 | 
			
		||||
      <.button>New Platform alias</.button>
 | 
			
		||||
    </.link>
 | 
			
		||||
  </:actions>
 | 
			
		||||
</.header>
 | 
			
		||||
 | 
			
		||||
<.table
 | 
			
		||||
  id="platform_aliases"
 | 
			
		||||
  rows={@platform_aliases}
 | 
			
		||||
  row_click={&JS.navigate(~p"/platform_aliases/#{&1}")}
 | 
			
		||||
>
 | 
			
		||||
  <:col :let={platform_alias} label="Url">{platform_alias.url}</:col>
 | 
			
		||||
  <:col :let={platform_alias} label="Platform">{platform_alias.platform.name}</:col>
 | 
			
		||||
 | 
			
		||||
  <:action :let={platform_alias}>
 | 
			
		||||
    <div class="sr-only">
 | 
			
		||||
      <.link navigate={~p"/platform_aliases/#{platform_alias}"}>Show</.link>
 | 
			
		||||
    </div>
 | 
			
		||||
    <.link navigate={~p"/platform_aliases/#{platform_alias}/edit"}>Edit</.link>
 | 
			
		||||
  </:action>
 | 
			
		||||
  <:action :let={platform_alias}>
 | 
			
		||||
    <.link
 | 
			
		||||
      href={~p"/platform_aliases/#{platform_alias}"}
 | 
			
		||||
      method="delete"
 | 
			
		||||
      data-confirm="Are you sure?"
 | 
			
		||||
    >
 | 
			
		||||
      Delete
 | 
			
		||||
    </.link>
 | 
			
		||||
  </:action>
 | 
			
		||||
</.table>
 | 
			
		||||
@ -0,0 +1,8 @@
 | 
			
		||||
<.header>
 | 
			
		||||
  New Platform alias
 | 
			
		||||
  <:subtitle>Use this form to manage platform_alias records in your database.</:subtitle>
 | 
			
		||||
</.header>
 | 
			
		||||
 | 
			
		||||
<.platform_alias_form changeset={@changeset} action={~p"/platform_aliases"} />
 | 
			
		||||
 | 
			
		||||
<.back navigate={~p"/platform_aliases"}>Back to platform_aliases</.back>
 | 
			
		||||
@ -0,0 +1,16 @@
 | 
			
		||||
<.simple_form :let={f} for={@changeset} action={@action}>
 | 
			
		||||
  <.error :if={@changeset.action}>
 | 
			
		||||
    Oops, something went wrong! Please check the errors below.
 | 
			
		||||
  </.error>
 | 
			
		||||
  <.input field={f[:url]} type="text" label="Url" />
 | 
			
		||||
  <.input
 | 
			
		||||
    field={f[:platform_id]}
 | 
			
		||||
    label="Platform"
 | 
			
		||||
    type="select"
 | 
			
		||||
    multiple={false}
 | 
			
		||||
    options={platform_opts(@changeset)}
 | 
			
		||||
  />
 | 
			
		||||
  <:actions>
 | 
			
		||||
    <.button>Save Platform alias</.button>
 | 
			
		||||
  </:actions>
 | 
			
		||||
</.simple_form>
 | 
			
		||||
@ -0,0 +1,16 @@
 | 
			
		||||
<.header>
 | 
			
		||||
  Platform alias {@platform_alias.id}
 | 
			
		||||
  <:subtitle>This is a platform_alias record from your database.</:subtitle>
 | 
			
		||||
  <:actions>
 | 
			
		||||
    <.link href={~p"/platform_aliases/#{@platform_alias}/edit"}>
 | 
			
		||||
      <.button>Edit platform_alias</.button>
 | 
			
		||||
    </.link>
 | 
			
		||||
  </:actions>
 | 
			
		||||
</.header>
 | 
			
		||||
 | 
			
		||||
<.list>
 | 
			
		||||
  <:item title="Url">{@platform_alias.url}</:item>
 | 
			
		||||
  <:item title="Platform">{@platform_alias.platform.name}</:item>
 | 
			
		||||
</.list>
 | 
			
		||||
 | 
			
		||||
<.back navigate={~p"/platform_aliases"}>Back to platform_aliases</.back>
 | 
			
		||||
@ -10,7 +10,7 @@
 | 
			
		||||
<.table id="platforms" rows={@platforms} row_click={&JS.navigate(~p"/platforms/#{&1}")}>
 | 
			
		||||
  <:col :let={platform} label="Name">{platform.name}</:col>
 | 
			
		||||
  <:col :let={platform} label="Url">{platform.url}</:col>
 | 
			
		||||
  <:col :let={platform} label="Icon">{raw(platform.icon)}</:col>
 | 
			
		||||
  <:col :let={platform} label="NSFW?">{platform.nsfw}</:col>
 | 
			
		||||
  <:action :let={platform}>
 | 
			
		||||
    <div class="sr-only">
 | 
			
		||||
      <.link navigate={~p"/platforms/#{platform}"}>Show</.link>
 | 
			
		||||
 | 
			
		||||
@ -3,8 +3,9 @@
 | 
			
		||||
    Oops, something went wrong! Please check the errors below.
 | 
			
		||||
  </.error>
 | 
			
		||||
  <.input field={f[:name]} type="text" label="Name" />
 | 
			
		||||
  <.input field={f[:slug]} type="text" label="Slug" />
 | 
			
		||||
  <.input field={f[:url]} type="text" label="Url" />
 | 
			
		||||
  <.input field={f[:icon]} type="text" label="Icon" />
 | 
			
		||||
  <.input field={f[:nsfw]} type="checkbox" label="NSFW?" />
 | 
			
		||||
  <:actions>
 | 
			
		||||
    <.button>Save Platform</.button>
 | 
			
		||||
  </:actions>
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,7 @@
 | 
			
		||||
<.list>
 | 
			
		||||
  <:item title="Name">{@platform.name}</:item>
 | 
			
		||||
  <:item title="Url">{@platform.url}</:item>
 | 
			
		||||
  <:item title="Icon">{raw(@platform.icon)}</:item>
 | 
			
		||||
  <:item title="NSFW?">{@platform.nsfw}</:item>
 | 
			
		||||
</.list>
 | 
			
		||||
 | 
			
		||||
<.back navigate={~p"/platforms"}>Back to platforms</.back>
 | 
			
		||||
 | 
			
		||||
@ -8,14 +8,12 @@
 | 
			
		||||
</.header>
 | 
			
		||||
 | 
			
		||||
<.table id="streams" rows={@streams} row_click={&JS.navigate(~p"/streams/#{&1}")}>
 | 
			
		||||
  <:col :let={stream} label="ID">{stream.id}</:col>
 | 
			
		||||
  <:col :let={stream} label="Title">{stream.title}</:col>
 | 
			
		||||
  <:col :let={stream} label="Date">{stream.date}</:col>
 | 
			
		||||
  <:col :let={stream} label="Title">{stream.title}</:col>
 | 
			
		||||
  <:col :let={stream} label="Platforms">
 | 
			
		||||
    <div class="columns is-1">
 | 
			
		||||
      <%= for platform <- stream.platforms do %>
 | 
			
		||||
        <div class="column is-mobile is-narrow">
 | 
			
		||||
          <%# {raw(platform.icon)} %>
 | 
			
		||||
          <.icon name={platform.slug} />
 | 
			
		||||
        </div>
 | 
			
		||||
      <% end %>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										62
									
								
								apps/bright/lib/bright_web/controllers/upload_controller.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								apps/bright/lib/bright_web/controllers/upload_controller.ex
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,62 @@
 | 
			
		||||
defmodule BrightWeb.UploadController do
 | 
			
		||||
  use BrightWeb, :controller
 | 
			
		||||
 | 
			
		||||
  alias Bright.Streams
 | 
			
		||||
  alias Bright.Streams.Upload
 | 
			
		||||
 | 
			
		||||
  def index(conn, _params) do
 | 
			
		||||
    uploads = Streams.list_uploads()
 | 
			
		||||
    render(conn, :index, uploads: uploads)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def new(conn, _params) do
 | 
			
		||||
    changeset = Streams.change_upload(%Upload{})
 | 
			
		||||
    render(conn, :new, changeset: changeset)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def create(conn, %{"upload" => upload_params}) do
 | 
			
		||||
    case Streams.create_upload(upload_params) do
 | 
			
		||||
      {:ok, upload} ->
 | 
			
		||||
        conn
 | 
			
		||||
        |> put_flash(:info, "Upload created successfully.")
 | 
			
		||||
        |> redirect(to: ~p"/uploads/#{upload}")
 | 
			
		||||
 | 
			
		||||
      {:error, %Ecto.Changeset{} = changeset} ->
 | 
			
		||||
        render(conn, :new, changeset: changeset)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def show(conn, %{"id" => id}) do
 | 
			
		||||
    upload = Streams.get_upload!(id)
 | 
			
		||||
    render(conn, :show, upload: upload)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def edit(conn, %{"id" => id}) do
 | 
			
		||||
    upload = Streams.get_upload!(id)
 | 
			
		||||
    changeset = Streams.change_upload(upload)
 | 
			
		||||
    render(conn, :edit, upload: upload, changeset: changeset)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def update(conn, %{"id" => id, "upload" => upload_params}) do
 | 
			
		||||
    upload = Streams.get_upload!(id)
 | 
			
		||||
 | 
			
		||||
    case Streams.update_upload(upload, upload_params) do
 | 
			
		||||
      {:ok, upload} ->
 | 
			
		||||
        conn
 | 
			
		||||
        |> put_flash(:info, "Upload updated successfully.")
 | 
			
		||||
        |> redirect(to: ~p"/uploads/#{upload}")
 | 
			
		||||
 | 
			
		||||
      {:error, %Ecto.Changeset{} = changeset} ->
 | 
			
		||||
        render(conn, :edit, upload: upload, changeset: changeset)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def delete(conn, %{"id" => id}) do
 | 
			
		||||
    upload = Streams.get_upload!(id)
 | 
			
		||||
    {:ok, _upload} = Streams.delete_upload(upload)
 | 
			
		||||
 | 
			
		||||
    conn
 | 
			
		||||
    |> put_flash(:info, "Upload deleted successfully.")
 | 
			
		||||
    |> redirect(to: ~p"/uploads")
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										13
									
								
								apps/bright/lib/bright_web/controllers/upload_html.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								apps/bright/lib/bright_web/controllers/upload_html.ex
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
			
		||||
defmodule BrightWeb.UploadHTML do
 | 
			
		||||
  use BrightWeb, :html
 | 
			
		||||
 | 
			
		||||
  embed_templates "upload_html/*"
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Renders a upload form.
 | 
			
		||||
  """
 | 
			
		||||
  attr :changeset, Ecto.Changeset, required: true
 | 
			
		||||
  attr :action, :string, required: true
 | 
			
		||||
 | 
			
		||||
  def upload_form(assigns)
 | 
			
		||||
end
 | 
			
		||||
@ -0,0 +1,8 @@
 | 
			
		||||
<.header>
 | 
			
		||||
  Edit Upload {@upload.id}
 | 
			
		||||
  <:subtitle>Use this form to manage upload records in your database.</:subtitle>
 | 
			
		||||
</.header>
 | 
			
		||||
 | 
			
		||||
<.upload_form changeset={@changeset} action={~p"/uploads/#{@upload}"} />
 | 
			
		||||
 | 
			
		||||
<.back navigate={~p"/uploads"}>Back to uploads</.back>
 | 
			
		||||
@ -0,0 +1,25 @@
 | 
			
		||||
<.header>
 | 
			
		||||
  Listing Uploads
 | 
			
		||||
  <:actions>
 | 
			
		||||
    <.link href={~p"/uploads/new"}>
 | 
			
		||||
      <.button>New Upload</.button>
 | 
			
		||||
    </.link>
 | 
			
		||||
  </:actions>
 | 
			
		||||
</.header>
 | 
			
		||||
 | 
			
		||||
<.table id="uploads" rows={@uploads} row_click={&JS.navigate(~p"/uploads/#{&1}")}>
 | 
			
		||||
  <:col :let={upload} label="Filename">{upload.filename}</:col>
 | 
			
		||||
  <:col :let={upload} label="Size">{upload.size}</:col>
 | 
			
		||||
  <:col :let={upload} label="Content type">{upload.content_type}</:col>
 | 
			
		||||
  <:action :let={upload}>
 | 
			
		||||
    <div class="sr-only">
 | 
			
		||||
      <.link navigate={~p"/uploads/#{upload}"}>Show</.link>
 | 
			
		||||
    </div>
 | 
			
		||||
    <.link navigate={~p"/uploads/#{upload}/edit"}>Edit</.link>
 | 
			
		||||
  </:action>
 | 
			
		||||
  <:action :let={upload}>
 | 
			
		||||
    <.link href={~p"/uploads/#{upload}"} method="delete" data-confirm="Are you sure?">
 | 
			
		||||
      Delete
 | 
			
		||||
    </.link>
 | 
			
		||||
  </:action>
 | 
			
		||||
</.table>
 | 
			
		||||
@ -0,0 +1,8 @@
 | 
			
		||||
<.header>
 | 
			
		||||
  New Upload
 | 
			
		||||
  <:subtitle>Use this form to manage upload records in your database.</:subtitle>
 | 
			
		||||
</.header>
 | 
			
		||||
 | 
			
		||||
<.upload_form changeset={@changeset} action={~p"/uploads"} />
 | 
			
		||||
 | 
			
		||||
<.back navigate={~p"/uploads"}>Back to uploads</.back>
 | 
			
		||||
@ -0,0 +1,17 @@
 | 
			
		||||
<.header>
 | 
			
		||||
  Upload {@upload.id}
 | 
			
		||||
  <:subtitle>This is a upload record from your database.</:subtitle>
 | 
			
		||||
  <:actions>
 | 
			
		||||
    <.link href={~p"/uploads/#{@upload}/edit"}>
 | 
			
		||||
      <.button>Edit upload</.button>
 | 
			
		||||
    </.link>
 | 
			
		||||
  </:actions>
 | 
			
		||||
</.header>
 | 
			
		||||
 | 
			
		||||
<.list>
 | 
			
		||||
  <:item title="Filename">{@upload.filename}</:item>
 | 
			
		||||
  <:item title="Size">{@upload.size}</:item>
 | 
			
		||||
  <:item title="Content type">{@upload.content_type}</:item>
 | 
			
		||||
</.list>
 | 
			
		||||
 | 
			
		||||
<.back navigate={~p"/uploads"}>Back to uploads</.back>
 | 
			
		||||
@ -0,0 +1,11 @@
 | 
			
		||||
<.simple_form :let={f} for={@changeset} action={@action}>
 | 
			
		||||
  <.error :if={@changeset.action}>
 | 
			
		||||
    Oops, something went wrong! Please check the errors below.
 | 
			
		||||
  </.error>
 | 
			
		||||
  <.input field={f[:filename]} type="text" label="Filename" />
 | 
			
		||||
  <.input field={f[:size]} type="number" label="Size" />
 | 
			
		||||
  <.input field={f[:content_type]} type="text" label="Content type" />
 | 
			
		||||
  <:actions>
 | 
			
		||||
    <.button>Save Upload</.button>
 | 
			
		||||
  </:actions>
 | 
			
		||||
</.simple_form>
 | 
			
		||||
@ -15,16 +15,19 @@
 | 
			
		||||
    </figure>
 | 
			
		||||
  </:col>
 | 
			
		||||
  <:col :let={vtuber} label="Archival">
 | 
			
		||||
 | 
			
		||||
    <% %{total_streams: total_streams, streams_with_vods: streams_with_vods, percentage: percentage} = Vtubers.vtuber_archive_percentage(vtuber) %>
 | 
			
		||||
    <% %{
 | 
			
		||||
      total_streams: total_streams,
 | 
			
		||||
      streams_with_vods: streams_with_vods,
 | 
			
		||||
      percentage: percentage
 | 
			
		||||
    } = Vtubers.vtuber_archive_percentage(vtuber) %>
 | 
			
		||||
    <div class="columns is-mobile">
 | 
			
		||||
      <div class="column is-two-thirds">
 | 
			
		||||
        <progress class="progress is-success" value={percentage} max="100">
 | 
			
		||||
          <%= percentage %>%
 | 
			
		||||
          {percentage}%
 | 
			
		||||
        </progress>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="column is-one-third">
 | 
			
		||||
        <%= streams_with_vods %> of <%= total_streams %> (<%= percentage %>%)
 | 
			
		||||
        {streams_with_vods} of {total_streams} ({percentage}%)
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </:col>
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,84 @@
 | 
			
		||||
defmodule BrightWeb.UploadLive.FormComponent do
 | 
			
		||||
  use BrightWeb, :live_component
 | 
			
		||||
 | 
			
		||||
  alias Bright.Streams
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def render(assigns) do
 | 
			
		||||
    ~H"""
 | 
			
		||||
    <div>
 | 
			
		||||
      <.header>
 | 
			
		||||
        {@title}
 | 
			
		||||
        <:subtitle>Use this form to manage upload records in your database.</:subtitle>
 | 
			
		||||
      </.header>
 | 
			
		||||
 | 
			
		||||
      <.simple_form
 | 
			
		||||
        for={@form}
 | 
			
		||||
        id="upload-form"
 | 
			
		||||
        phx-target={@myself}
 | 
			
		||||
        phx-change="validate"
 | 
			
		||||
        phx-submit="save"
 | 
			
		||||
      >
 | 
			
		||||
        <.input field={@form[:filename]} type="text" label="Filename" />
 | 
			
		||||
        <.input field={@form[:size]} type="number" label="Size" />
 | 
			
		||||
        <.input field={@form[:content_type]} type="text" label="Content type" />
 | 
			
		||||
        <:actions>
 | 
			
		||||
          <.button phx-disable-with="Saving...">Save Upload</.button>
 | 
			
		||||
        </:actions>
 | 
			
		||||
      </.simple_form>
 | 
			
		||||
    </div>
 | 
			
		||||
    """
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def update(%{upload: upload} = assigns, socket) do
 | 
			
		||||
    {:ok,
 | 
			
		||||
     socket
 | 
			
		||||
     |> assign(assigns)
 | 
			
		||||
     |> assign_new(:form, fn ->
 | 
			
		||||
       to_form(Streams.change_upload(upload))
 | 
			
		||||
     end)}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def handle_event("validate", %{"upload" => upload_params}, socket) do
 | 
			
		||||
    changeset = Streams.change_upload(socket.assigns.upload, upload_params)
 | 
			
		||||
    {:noreply, assign(socket, form: to_form(changeset, action: :validate))}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_event("save", %{"upload" => upload_params}, socket) do
 | 
			
		||||
    save_upload(socket, socket.assigns.action, upload_params)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp save_upload(socket, :edit, upload_params) do
 | 
			
		||||
    case Streams.update_upload(socket.assigns.upload, upload_params) do
 | 
			
		||||
      {:ok, upload} ->
 | 
			
		||||
        notify_parent({:saved, upload})
 | 
			
		||||
 | 
			
		||||
        {:noreply,
 | 
			
		||||
         socket
 | 
			
		||||
         |> put_flash(:info, "Upload updated successfully")
 | 
			
		||||
         |> push_patch(to: socket.assigns.patch)}
 | 
			
		||||
 | 
			
		||||
      {:error, %Ecto.Changeset{} = changeset} ->
 | 
			
		||||
        {:noreply, assign(socket, form: to_form(changeset))}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp save_upload(socket, :new, upload_params) do
 | 
			
		||||
    case Streams.create_upload(upload_params) do
 | 
			
		||||
      {:ok, upload} ->
 | 
			
		||||
        notify_parent({:saved, upload})
 | 
			
		||||
 | 
			
		||||
        {:noreply,
 | 
			
		||||
         socket
 | 
			
		||||
         |> put_flash(:info, "Upload created successfully")
 | 
			
		||||
         |> push_patch(to: socket.assigns.patch)}
 | 
			
		||||
 | 
			
		||||
      {:error, %Ecto.Changeset{} = changeset} ->
 | 
			
		||||
        {:noreply, assign(socket, form: to_form(changeset))}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										331
									
								
								apps/bright/lib/bright_web/live/upload_live/index.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										331
									
								
								apps/bright/lib/bright_web/live/upload_live/index.ex
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,331 @@
 | 
			
		||||
defmodule BrightWeb.UploadLive.Index do
 | 
			
		||||
  use BrightWeb, :live_view
 | 
			
		||||
 | 
			
		||||
  alias Bright.Streams
 | 
			
		||||
  alias Bright.Streams.Upload
 | 
			
		||||
  alias Bright.Utils
 | 
			
		||||
  require Logger
 | 
			
		||||
 | 
			
		||||
  def mount(_params, _session, socket) do
 | 
			
		||||
    # {:ok,
 | 
			
		||||
    #  socket
 | 
			
		||||
    #  |> assign(:uploaded_files, [])
 | 
			
		||||
    #  |> allow_upload(:vods,
 | 
			
		||||
    #    accept: ~w(.mp4 .mov .ts .avi .mpeg .ogv .webm .3gp .3g2),
 | 
			
		||||
    #    max_entries: 3,
 | 
			
		||||
    #    max_file_size: 80 * 1_000_000_000,
 | 
			
		||||
    #    external: &presign_upload/2
 | 
			
		||||
    #  )}
 | 
			
		||||
 | 
			
		||||
    # socket = assign(socket, endpoint: System.fetch_env!("UPPY_ENDPOINT"))
 | 
			
		||||
    socket =
 | 
			
		||||
      socket
 | 
			
		||||
      |> assign(:uploaded_files, [])
 | 
			
		||||
 | 
			
		||||
    {:ok, socket}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def render(assigns) do
 | 
			
		||||
    ~H"""
 | 
			
		||||
    <div>
 | 
			
		||||
 | 
			
		||||
      <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/uppy/4.12.2/uppy.min.css" integrity="sha512-oPlr9/HXIlp7YoIRNsexheOu2/P2sEVi8EFQEAWUlHHijx0QbQ9qgihNYmIYtdJP3xOIMbZcnSVhrznIh5DKkg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
      <div id="uppy-dashboard" phx-hook="UppyHook" phx-update="ignore"></div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    </div>
 | 
			
		||||
    """
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # @impl true
 | 
			
		||||
  # def handle_event(
 | 
			
		||||
  #       "presign",
 | 
			
		||||
  #       %{"type" => type, "name" => name},
 | 
			
		||||
  #       socket
 | 
			
		||||
  #     ) do
 | 
			
		||||
  #   {:ok, %{url: url}, socket} = presign_url(name, type, socket)
 | 
			
		||||
  #   Logger.debug("✍️✍️✍️ presign called with name=#{name} type=#{type}. url=#{url}")
 | 
			
		||||
  #   # socket = assign(socket, signatures: {name, url})
 | 
			
		||||
  #   # socket =
 | 
			
		||||
  #   #   socket
 | 
			
		||||
  #   #   |> update(:uploaded_files, &(&1 ++ uploaded_files))
 | 
			
		||||
 | 
			
		||||
  #   # |> push_event("process_upload", %{name: name, url: url, type: type})
 | 
			
		||||
 | 
			
		||||
  #   {:reply, %{name: name, type: type, url: url}, socket}
 | 
			
		||||
  # end
 | 
			
		||||
 | 
			
		||||
  # @impl true
 | 
			
		||||
  # def handle_event("upload_videos", _params, socket) do
 | 
			
		||||
  #   uploaded_files =
 | 
			
		||||
  #     consume_uploaded_entries(socket, :vods, fn %{path: _path}, entry ->
 | 
			
		||||
  #       IO.puts("@todo ⭐⭐⭐ Handling #{entry.client_type} @todo @todo @todo")
 | 
			
		||||
  #       IO.puts(inspect(entry))
 | 
			
		||||
  #     end)
 | 
			
		||||
 | 
			
		||||
  #   socket =
 | 
			
		||||
  #     socket
 | 
			
		||||
  #     |> update(:uploaded_files, &(&1 ++ uploaded_files))
 | 
			
		||||
 | 
			
		||||
  #   {:noreply, socket}
 | 
			
		||||
  # end
 | 
			
		||||
 | 
			
		||||
  # def handle_event("update_preview_srcs", %{"srcs" => srcs}, socket) do
 | 
			
		||||
  #   uploaded_files =
 | 
			
		||||
  #     socket.assigns.uploaded_files
 | 
			
		||||
  #     |> Enum.map(fn entry ->
 | 
			
		||||
  #       if Map.has_key?(srcs, entry.ref) do
 | 
			
		||||
  #         entry
 | 
			
		||||
  #         |> Map.put(:preview_src, Map.fetch!(srcs, entry.ref))
 | 
			
		||||
  #       else
 | 
			
		||||
  #         entry
 | 
			
		||||
  #       end
 | 
			
		||||
  #     end)
 | 
			
		||||
 | 
			
		||||
  #   socket =
 | 
			
		||||
  #     socket
 | 
			
		||||
  #     |> assign(:uploaded_files, uploaded_files)
 | 
			
		||||
 | 
			
		||||
  #   {:noreply, socket}
 | 
			
		||||
  # end
 | 
			
		||||
 | 
			
		||||
  # def handle_event("validate_upload", _params, socket) do
 | 
			
		||||
  #   num_remaining_uploads =
 | 
			
		||||
  #     length(socket.assigns.uploaded_files) - socket.assigns.uploads.vods.max_entries
 | 
			
		||||
 | 
			
		||||
  #   valid =
 | 
			
		||||
  #     Enum.uniq_by(socket.assigns.uploads.vods.entries, & &1.client_name)
 | 
			
		||||
  #     |> Enum.take(num_remaining_uploads)
 | 
			
		||||
 | 
			
		||||
  #   socket =
 | 
			
		||||
  #     Enum.reduce(socket.assigns.uploads.vods.entries, socket, fn entry, socket ->
 | 
			
		||||
  #       if entry in valid do
 | 
			
		||||
  #         socket
 | 
			
		||||
  #       else
 | 
			
		||||
  #         socket
 | 
			
		||||
  #         |> cancel_upload(:vods, entry.ref)
 | 
			
		||||
  #         |> put_flash(
 | 
			
		||||
  #           :error,
 | 
			
		||||
  #           "Uploaded files should be unique and cannot exceed #{socket.assigns.uploads.vods.max_entries} total files."
 | 
			
		||||
  #         )
 | 
			
		||||
  #       end
 | 
			
		||||
  #     end)
 | 
			
		||||
 | 
			
		||||
  #   {:noreply, socket}
 | 
			
		||||
  # end
 | 
			
		||||
 | 
			
		||||
  # def handle_event("cancel_upload", %{"ref" => ref}, socket) do
 | 
			
		||||
  #   {:noreply, cancel_upload(socket, :vods, ref)}
 | 
			
		||||
  # end
 | 
			
		||||
 | 
			
		||||
  # def handle_event("cancel_upload", _params, socket) do
 | 
			
		||||
  #   socket =
 | 
			
		||||
  #     Enum.reduce(socket.assigns.uploads.vods.entries, socket, fn entry, socket ->
 | 
			
		||||
  #       cancel_upload(socket, :vods, entry.ref)
 | 
			
		||||
  #     end)
 | 
			
		||||
 | 
			
		||||
  #   {:noreply, socket}
 | 
			
		||||
  # end
 | 
			
		||||
 | 
			
		||||
  # def presign_upload(name, type, socket) do
 | 
			
		||||
  #   Logger.debug("presign_upload was called with name=#{inspect(name)} and type=#{inspect(type)}")
 | 
			
		||||
  #   config = ExAws.Config.new(:s3)
 | 
			
		||||
  #   bucket = System.fetch_env!("AWS_BUCKET")
 | 
			
		||||
  #   key = "usc/#{Utils.random_string()}/#{name}"
 | 
			
		||||
 | 
			
		||||
  #   {:ok, url} =
 | 
			
		||||
  #     ExAws.S3.presigned_url(config, :put, bucket, key,
 | 
			
		||||
  #       expires_in: 3600,
 | 
			
		||||
  #       query_params: [{"Content-Type", type}]
 | 
			
		||||
  #     )
 | 
			
		||||
 | 
			
		||||
  #   {:ok, %{uploader: "S3", key: key, url: url}, socket}
 | 
			
		||||
  # end
 | 
			
		||||
 | 
			
		||||
  # # @doc @see https://hexdocs.pm/ex_aws_s3/ExAws.S3.html#presigned_post/4
 | 
			
		||||
  # def presigned_post(name, socket) do
 | 
			
		||||
  #   Logger.debug("presigned_post with name=#{inspect(name)}")
 | 
			
		||||
  #   config = ExAws.Config.new(:s3)
 | 
			
		||||
  #   bucket = System.fetch_env!("AWS_BUCKET")
 | 
			
		||||
  #   key = "usc/#{Utils.random_string()}/#{name}"
 | 
			
		||||
  #   {:ok, url} = ExAws.S3.presigned_post(config, )
 | 
			
		||||
  # end
 | 
			
		||||
 | 
			
		||||
  defp get_s3_config(name) do
 | 
			
		||||
    config = ExAws.Config.new(:s3)
 | 
			
		||||
    bucket = System.fetch_env!("AWS_BUCKET")
 | 
			
		||||
    key = "usc/#{Utils.random_string()}/#{name}"
 | 
			
		||||
    %{config: config, bucket: bucket, key: key}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_event(
 | 
			
		||||
        "list_parts",
 | 
			
		||||
        %{"upload_id" => upload_id, "key" => key},
 | 
			
		||||
        socket
 | 
			
		||||
      ) do
 | 
			
		||||
    config = ExAws.Config.new(:s3)
 | 
			
		||||
    bucket = System.fetch_env!("AWS_BUCKET")
 | 
			
		||||
    # key = "usc/#{Utils.random_string()}/#{name}"
 | 
			
		||||
    # %{config: config, bucket: bucket, key: key} = get_s3_config(name)
 | 
			
		||||
    case ExAws.S3.list_parts(bucket, key, upload_id) do
 | 
			
		||||
      # <Part>
 | 
			
		||||
      # <ETag>"85f30635602dc09bd85957a6e82a2c21"</ETag>
 | 
			
		||||
      # <LastModified>2023-08-31T18:54:55.693Z</LastModified>
 | 
			
		||||
      # <PartNumber>1</PartNumber>
 | 
			
		||||
      # <Size>11</Size>
 | 
			
		||||
 | 
			
		||||
      {:ok, part: %{etag: etag, partNumber: partNumber, size: size}} ->
 | 
			
		||||
        Logger.debug("🦠🦠🦠 we got an etag from list_parts. etag=#{inspect(etag)}")
 | 
			
		||||
        {:reply, %{etag: etag, partNumber: partNumber, size: size}, socket}
 | 
			
		||||
 | 
			
		||||
      {:error, reason} ->
 | 
			
		||||
        Logger.error("error while running S3.list_parts. reason=#{inspect(reason)}")
 | 
			
		||||
        {:reply, %{error: "Failed to list_parts"}, socket}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # @doc @see https://hexdocs.pm/ex_aws_s3/ExAws.S3.html#initiate_multipart_upload/3
 | 
			
		||||
  def handle_event(
 | 
			
		||||
        "initiate_multipart_upload",
 | 
			
		||||
        %{"name" => name, "type" => type},
 | 
			
		||||
        socket
 | 
			
		||||
      ) do
 | 
			
		||||
    %{config: config, bucket: bucket, key: key} = get_s3_config(name)
 | 
			
		||||
 | 
			
		||||
    operation = ExAws.S3.initiate_multipart_upload(bucket, key, content_type: type)
 | 
			
		||||
 | 
			
		||||
    case ExAws.request(operation, config) do
 | 
			
		||||
      {:ok, %{body: %{key: key, upload_id: upload_id}} = response} ->
 | 
			
		||||
        Logger.debug(
 | 
			
		||||
          "Multipart upload initiated. Upload ID: #{upload_id}, Key: #{key}, response=#{inspect(response)}"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        {:reply, %{upload_id: upload_id, key: key}, socket}
 | 
			
		||||
 | 
			
		||||
      {:error, reason} ->
 | 
			
		||||
        Logger.error("Failed to initiate multipart upload: #{inspect(reason)}")
 | 
			
		||||
        {:reply, %{error: "Failed to initiate upload"}, socket}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_event(
 | 
			
		||||
        "abort_multipart_upload",
 | 
			
		||||
        %{"key" => key, "uploadId" => upload_id},
 | 
			
		||||
        socket
 | 
			
		||||
      ) do
 | 
			
		||||
    config = ExAws.Config.new(:s3)
 | 
			
		||||
    bucket = System.fetch_env!("AWS_BUCKET")
 | 
			
		||||
 | 
			
		||||
    operation = ExAws.S3.abort_multipart_upload(bucket, key, upload_id)
 | 
			
		||||
 | 
			
		||||
    case ExAws.request(operation, config) do
 | 
			
		||||
      {:ok, _} ->
 | 
			
		||||
        Logger.debug("Aborting multipart upload. Upload ID: #{upload_id}}")
 | 
			
		||||
        {:reply, %{upload_id: upload_id}, socket}
 | 
			
		||||
 | 
			
		||||
      {:error, reason} ->
 | 
			
		||||
        Logger.error("Failed to abort multipart upload: #{inspect(reason)}")
 | 
			
		||||
        {:reply, %{error: "Failed to initiate upload"}, socket}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_event(
 | 
			
		||||
        "get_upload_parameters",
 | 
			
		||||
        %{"type" => type, "name" => name},
 | 
			
		||||
        socket
 | 
			
		||||
      ) do
 | 
			
		||||
    %{config: config, bucket: bucket, key: key} = get_s3_config(name)
 | 
			
		||||
 | 
			
		||||
    {:ok, url} =
 | 
			
		||||
      ExAws.S3.presigned_url(config, :put, bucket, key,
 | 
			
		||||
        expires_in: 3600,
 | 
			
		||||
        query_params: [
 | 
			
		||||
          {"Content-Type", type}
 | 
			
		||||
        ]
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
    {:reply, %{type: type, method: "PUT", url: url}, socket}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_event(
 | 
			
		||||
        "complete_multipart_upload",
 | 
			
		||||
        %{"key" => key, "uploadId" => upload_id, "parts" => parts},
 | 
			
		||||
        socket
 | 
			
		||||
      ) do
 | 
			
		||||
    config = ExAws.Config.new(:s3)
 | 
			
		||||
    bucket = System.fetch_env!("AWS_BUCKET")
 | 
			
		||||
 | 
			
		||||
    Logger.debug("(before) parts=#{inspect(parts)}")
 | 
			
		||||
 | 
			
		||||
    parts =
 | 
			
		||||
      Enum.map(parts, fn part ->
 | 
			
		||||
        {part["part_number"], part["etag"]}
 | 
			
		||||
      end)
 | 
			
		||||
 | 
			
		||||
    operation = ExAws.S3.complete_multipart_upload(bucket, key, upload_id, parts)
 | 
			
		||||
 | 
			
		||||
    case ExAws.request(operation, config) do
 | 
			
		||||
      {:ok, %{body: %{location: location}} = response} ->
 | 
			
		||||
        Logger.debug(
 | 
			
		||||
          "Completing multipart upload. Upload ID: #{upload_id}, response=#{inspect(response)}"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        {:reply, %{location: location}, socket}
 | 
			
		||||
 | 
			
		||||
      {:error, reason} ->
 | 
			
		||||
        Logger.error("Failed to complete multipart upload: #{inspect(reason)}")
 | 
			
		||||
        {:reply, %{error: "The server failed to complete multipart upload"}, socket}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_event(
 | 
			
		||||
        "sign_part",
 | 
			
		||||
        %{"body" => _body, "key" => key, "partNumber" => part_number, "uploadId" => upload_id},
 | 
			
		||||
        socket
 | 
			
		||||
      ) do
 | 
			
		||||
    Logger.debug(
 | 
			
		||||
      "sign_part was called with key=#{inspect(key)} part_number=#{inspect(part_number)} upload_id=#{inspect(upload_id)}"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    config = ExAws.Config.new(:s3)
 | 
			
		||||
    bucket = System.fetch_env!("AWS_BUCKET")
 | 
			
		||||
 | 
			
		||||
    {:ok, url} =
 | 
			
		||||
      ExAws.S3.presigned_url(config, :put, bucket, key,
 | 
			
		||||
        expires_in: 3600,
 | 
			
		||||
        query_params: [
 | 
			
		||||
          {"partNumber", part_number},
 | 
			
		||||
          {"uploadId", upload_id}
 | 
			
		||||
        ]
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
    {:reply, %{url: url}, socket}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp join_refs(entries), do: Enum.join(entries, ",")
 | 
			
		||||
  def error_to_string(:too_large), do: "File too large!"
 | 
			
		||||
  def error_to_string(:not_accepted), do: "Bad file type!"
 | 
			
		||||
 | 
			
		||||
  defp to_megabytes_or_kilobytes(bytes) when is_integer(bytes) do
 | 
			
		||||
    case bytes do
 | 
			
		||||
      b when b < 1_048_576 ->
 | 
			
		||||
        kilobytes = (b / 1024) |> Float.round(1)
 | 
			
		||||
 | 
			
		||||
        if kilobytes < 1 do
 | 
			
		||||
          "#{kilobytes}MB"
 | 
			
		||||
        else
 | 
			
		||||
          "#{round(kilobytes)}KB"
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
      _ ->
 | 
			
		||||
        megabytes = (bytes / 1_048_576) |> Float.round(1)
 | 
			
		||||
        "#{megabytes}MB"
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										21
									
								
								apps/bright/lib/bright_web/live/upload_live/show.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								apps/bright/lib/bright_web/live/upload_live/show.ex
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
			
		||||
defmodule BrightWeb.UploadLive.Show do
 | 
			
		||||
  use BrightWeb, :live_view
 | 
			
		||||
 | 
			
		||||
  alias Bright.Streams
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def mount(_params, _session, socket) do
 | 
			
		||||
    {:ok, socket}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def handle_params(%{"id" => id}, _, socket) do
 | 
			
		||||
    {:noreply,
 | 
			
		||||
     socket
 | 
			
		||||
     |> assign(:page_title, page_title(socket.assigns.live_action))
 | 
			
		||||
     |> assign(:upload, Streams.get_upload!(id))}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp page_title(:show), do: "Show Upload"
 | 
			
		||||
  defp page_title(:edit), do: "Edit Upload"
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										28
									
								
								apps/bright/lib/bright_web/live/upload_live/show.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								apps/bright/lib/bright_web/live/upload_live/show.html.heex
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
			
		||||
<.header>
 | 
			
		||||
  Upload {@upload.id}
 | 
			
		||||
  <:subtitle>This is a upload record from your database.</:subtitle>
 | 
			
		||||
  <:actions>
 | 
			
		||||
    <.link patch={~p"/uploads/#{@upload}/show/edit"} phx-click={JS.push_focus()}>
 | 
			
		||||
      <.button>Edit upload</.button>
 | 
			
		||||
    </.link>
 | 
			
		||||
  </:actions>
 | 
			
		||||
</.header>
 | 
			
		||||
 | 
			
		||||
<.list>
 | 
			
		||||
  <:item title="Filename">{@upload.filename}</:item>
 | 
			
		||||
  <:item title="Size">{@upload.size}</:item>
 | 
			
		||||
  <:item title="Content type">{@upload.content_type}</:item>
 | 
			
		||||
</.list>
 | 
			
		||||
 | 
			
		||||
<.back navigate={~p"/uploads"}>Back to uploads</.back>
 | 
			
		||||
 | 
			
		||||
<.modal :if={@live_action == :edit} id="upload-modal" show on_cancel={JS.patch(~p"/uploads/#{@upload}")}>
 | 
			
		||||
  <.live_component
 | 
			
		||||
    module={BrightWeb.UploadLive.FormComponent}
 | 
			
		||||
    id={@upload.id}
 | 
			
		||||
    title={@page_title}
 | 
			
		||||
    action={@live_action}
 | 
			
		||||
    upload={@upload}
 | 
			
		||||
    patch={~p"/uploads/#{@upload}"}
 | 
			
		||||
  />
 | 
			
		||||
</.modal>
 | 
			
		||||
							
								
								
									
										129
									
								
								apps/bright/lib/bright_web/live/upload_live/simple_s3_upload.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								apps/bright/lib/bright_web/live/upload_live/simple_s3_upload.ex
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,129 @@
 | 
			
		||||
defmodule SimpleS3Upload do
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  Dependency-free S3 Form Upload using HTTP POST sigv4
 | 
			
		||||
 | 
			
		||||
  @see https://gist.github.com/chrismccord/37862f1f8b1f5148644b75d20d1cb073
 | 
			
		||||
 | 
			
		||||
  https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-post-example.html
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Signs a form upload.
 | 
			
		||||
 | 
			
		||||
  The configuration is a map which must contain the following keys:
 | 
			
		||||
 | 
			
		||||
    * `:region` - The AWS region, such as "us-east-1"
 | 
			
		||||
    * `:access_key_id` - The AWS access key id
 | 
			
		||||
    * `:secret_access_key` - The AWS secret access key
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  Returns a map of form fields to be used on the client via the JavaScript `FormData` API.
 | 
			
		||||
 | 
			
		||||
  ## Options
 | 
			
		||||
 | 
			
		||||
    * `:key` - The required key of the object to be uploaded.
 | 
			
		||||
    * `:max_file_size` - The required maximum allowed file size in bytes.
 | 
			
		||||
    * `:content_type` - The required MIME type of the file to be uploaded.
 | 
			
		||||
    * `:expires_in` - The required expiration time in milliseconds from now
 | 
			
		||||
      before the signed upload expires.
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
 | 
			
		||||
      config = %{
 | 
			
		||||
        region: "us-east-1",
 | 
			
		||||
        access_key_id: System.fetch_env!("AWS_ACCESS_KEY_ID"),
 | 
			
		||||
        secret_access_key: System.fetch_env!("AWS_SECRET_ACCESS_KEY")
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      {:ok, fields} =
 | 
			
		||||
        SimpleS3Upload.sign_form_upload(config, "my-bucket",
 | 
			
		||||
          key: "public/my-file-name",
 | 
			
		||||
          content_type: "image/png",
 | 
			
		||||
          max_file_size: 10_000,
 | 
			
		||||
          expires_in: :timer.hours(1)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  def sign_form_upload(config, bucket, opts) do
 | 
			
		||||
    key = Keyword.fetch!(opts, :key)
 | 
			
		||||
    max_file_size = Keyword.fetch!(opts, :max_file_size)
 | 
			
		||||
    content_type = Keyword.fetch!(opts, :content_type)
 | 
			
		||||
    expires_in = Keyword.fetch!(opts, :expires_in)
 | 
			
		||||
 | 
			
		||||
    expires_at = DateTime.add(DateTime.utc_now(), expires_in, :millisecond)
 | 
			
		||||
    amz_date = amz_date(expires_at)
 | 
			
		||||
    credential = credential(config, expires_at)
 | 
			
		||||
 | 
			
		||||
    encoded_policy =
 | 
			
		||||
      Base.encode64("""
 | 
			
		||||
      {
 | 
			
		||||
        "expiration": "#{DateTime.to_iso8601(expires_at)}",
 | 
			
		||||
        "conditions": [
 | 
			
		||||
          {"bucket":  "#{bucket}"},
 | 
			
		||||
          ["eq", "$key", "#{key}"],
 | 
			
		||||
          {"acl": "public-read"},
 | 
			
		||||
          ["eq", "$Content-Type", "#{content_type}"],
 | 
			
		||||
          ["content-length-range", 0, #{max_file_size}],
 | 
			
		||||
          {"x-amz-server-side-encryption": "AES256"},
 | 
			
		||||
          {"x-amz-credential": "#{credential}"},
 | 
			
		||||
          {"x-amz-algorithm": "AWS4-HMAC-SHA256"},
 | 
			
		||||
          {"x-amz-date": "#{amz_date}"}
 | 
			
		||||
        ]
 | 
			
		||||
      }
 | 
			
		||||
      """)
 | 
			
		||||
 | 
			
		||||
    fields = %{
 | 
			
		||||
      "key" => key,
 | 
			
		||||
      "acl" => "public-read",
 | 
			
		||||
      "content-type" => content_type,
 | 
			
		||||
      "x-amz-server-side-encryption" => "AES256",
 | 
			
		||||
      "x-amz-credential" => credential,
 | 
			
		||||
      "x-amz-algorithm" => "AWS4-HMAC-SHA256",
 | 
			
		||||
      "x-amz-date" => amz_date,
 | 
			
		||||
      "policy" => encoded_policy,
 | 
			
		||||
      "x-amz-signature" => signature(config, expires_at, encoded_policy)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    {:ok, fields}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp amz_date(time) do
 | 
			
		||||
    time
 | 
			
		||||
    |> NaiveDateTime.to_iso8601()
 | 
			
		||||
    |> String.split(".")
 | 
			
		||||
    |> List.first()
 | 
			
		||||
    |> String.replace("-", "")
 | 
			
		||||
    |> String.replace(":", "")
 | 
			
		||||
    |> Kernel.<>("Z")
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp credential(%{} = config, %DateTime{} = expires_at) do
 | 
			
		||||
    "#{config.access_key_id}/#{short_date(expires_at)}/#{config.region}/s3/aws4_request"
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp signature(config, %DateTime{} = expires_at, encoded_policy) do
 | 
			
		||||
    config
 | 
			
		||||
    |> signing_key(expires_at, "s3")
 | 
			
		||||
    |> sha256(encoded_policy)
 | 
			
		||||
    |> Base.encode16(case: :lower)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp signing_key(%{} = config, %DateTime{} = expires_at, service) when service in ["s3"] do
 | 
			
		||||
    amz_date = short_date(expires_at)
 | 
			
		||||
    %{secret_access_key: secret, region: region} = config
 | 
			
		||||
 | 
			
		||||
    ("AWS4" <> secret)
 | 
			
		||||
    |> sha256(amz_date)
 | 
			
		||||
    |> sha256(region)
 | 
			
		||||
    |> sha256(service)
 | 
			
		||||
    |> sha256("aws4_request")
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp short_date(%DateTime{} = expires_at) do
 | 
			
		||||
    expires_at
 | 
			
		||||
    |> amz_date()
 | 
			
		||||
    |> String.slice(0..7)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp sha256(secret, msg), do: :crypto.mac(:hmac, :sha256, secret, msg)
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										39
									
								
								apps/bright/lib/bright_web/live/upload_live/upload_live.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								apps/bright/lib/bright_web/live/upload_live/upload_live.ex
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,39 @@
 | 
			
		||||
# # lib/my_app_web/live/upload_live.ex
 | 
			
		||||
# defmodule MyAppWeb.UploadLive do
 | 
			
		||||
#   use MyAppWeb, :live_view
 | 
			
		||||
 | 
			
		||||
#   @impl Phoenix.LiveView
 | 
			
		||||
#   def mount(_params, _session, socket) do
 | 
			
		||||
#     {:ok,
 | 
			
		||||
#      socket
 | 
			
		||||
#      |> assign(:uploaded_files, [])
 | 
			
		||||
#      |> allow_upload(:avatar, accept: ~w(.jpg .jpeg), max_entries: 2)}
 | 
			
		||||
#   end
 | 
			
		||||
 | 
			
		||||
#   @impl Phoenix.LiveView
 | 
			
		||||
#   def handle_event("validate", _params, socket) do
 | 
			
		||||
#     {:noreply, socket}
 | 
			
		||||
#   end
 | 
			
		||||
 | 
			
		||||
#   @impl Phoenix.LiveView
 | 
			
		||||
#   def handle_event("cancel-upload", %{"ref" => ref}, socket) do
 | 
			
		||||
#     {:noreply, cancel_upload(socket, :avatar, ref)}
 | 
			
		||||
#   end
 | 
			
		||||
 | 
			
		||||
#   @impl Phoenix.LiveView
 | 
			
		||||
#   def handle_event("save", _params, socket) do
 | 
			
		||||
#     uploaded_files =
 | 
			
		||||
#       consume_uploaded_entries(socket, :avatar, fn %{path: path}, _entry ->
 | 
			
		||||
#         dest = Path.join([:code.priv_dir(:my_app), "static", "uploads", Path.basename(path)])
 | 
			
		||||
#         # You will need to create `priv/static/uploads` for `File.cp!/2` to work.
 | 
			
		||||
#         File.cp!(path, dest)
 | 
			
		||||
#         {:ok, ~p"/uploads/#{Path.basename(dest)}"}
 | 
			
		||||
#       end)
 | 
			
		||||
 | 
			
		||||
#     {:noreply, update(socket, :uploaded_files, &(&1 ++ uploaded_files))}
 | 
			
		||||
#   end
 | 
			
		||||
 | 
			
		||||
#   defp error_to_string(:too_large), do: "Too large"
 | 
			
		||||
#   defp error_to_string(:too_many_files), do: "You have selected too many files"
 | 
			
		||||
#   defp error_to_string(:not_accepted), do: "You have selected an unacceptable file type"
 | 
			
		||||
# end
 | 
			
		||||
@ -75,6 +75,7 @@ defmodule BrightWeb.Router do
 | 
			
		||||
    #   get "/vtubers/:id/edit", VtuberController, :edit
 | 
			
		||||
    # end
 | 
			
		||||
 | 
			
		||||
    resources("/uploads", UploadController, only: [:show, :index, :delete])
 | 
			
		||||
    resources("/vods", VodController, only: [:create, :new, :edit, :update, :delete])
 | 
			
		||||
    resources("/vtubers", VtuberController, only: [:delete])
 | 
			
		||||
 | 
			
		||||
@ -82,10 +83,12 @@ defmodule BrightWeb.Router do
 | 
			
		||||
 | 
			
		||||
    resources("/torrents", TorrentController, only: [:new, :create, :edit, :update, :delete])
 | 
			
		||||
 | 
			
		||||
    ## !!! DANGER, platforms must only be writable by admins, (unless we implement SVG sanitizing)
 | 
			
		||||
    ## @todo remove SVGs from the database and instead put them in assets
 | 
			
		||||
    resources("/platforms", PlatformController, only: [:new, :create, :edit, :update, :delete])
 | 
			
		||||
 | 
			
		||||
    resources("/platform_aliases", PlatformAliasController,
 | 
			
		||||
      only: [:new, :create, :edit, :update, :delete]
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    oban_dashboard("/oban")
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
@ -112,6 +115,9 @@ defmodule BrightWeb.Router do
 | 
			
		||||
    get("/platforms", PlatformController, :index)
 | 
			
		||||
    get("/platforms/:id", PlatformController, :show)
 | 
			
		||||
 | 
			
		||||
    get("/platform_aliases", PlatformAliasController, :index)
 | 
			
		||||
    get("/platform_aliases/:id", PlatformAliasController, :show)
 | 
			
		||||
 | 
			
		||||
    resources("/vtubers", VtuberController, only: [:index, :show])
 | 
			
		||||
 | 
			
		||||
    resources "/vt", VtuberController do
 | 
			
		||||
@ -122,6 +128,8 @@ defmodule BrightWeb.Router do
 | 
			
		||||
    live_session :authenticated,
 | 
			
		||||
      on_mount: [{BrightWeb.AuthController, :ensure_authenticated}] do
 | 
			
		||||
      live("/profile", ProfileLive)
 | 
			
		||||
      live("/upload", UploadLive.Index, :index)
 | 
			
		||||
      # live("/upload/presign", , :)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -47,7 +47,7 @@ defmodule Bright.MixProject do
 | 
			
		||||
      {:phoenix_live_view, "~> 1.0"},
 | 
			
		||||
      {:floki, ">= 0.30.0", only: :test},
 | 
			
		||||
      {:phoenix_live_dashboard, "~> 0.8.3"},
 | 
			
		||||
      {:esbuild, "~> 0.8", runtime: Mix.env() == :dev},
 | 
			
		||||
      {:esbuild, "~> 0.9.0", runtime: Mix.env() == :dev},
 | 
			
		||||
      {:dart_sass, "0.7.0", runtime: Mix.env() == :dev},
 | 
			
		||||
      {:bulma, "1.0.2"},
 | 
			
		||||
      {:swoosh, "~> 1.5"},
 | 
			
		||||
@ -74,7 +74,7 @@ defmodule Bright.MixProject do
 | 
			
		||||
      {:bento, "~> 1.0"},
 | 
			
		||||
      {:identicon_svg, "~> 0.9"},
 | 
			
		||||
      {:excoveralls, "~> 0.18", only: :test},
 | 
			
		||||
      {:quinn, "~> 1.1.3"},
 | 
			
		||||
      {:quinn, "~> 1.1.3"}
 | 
			
		||||
    ]
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@
 | 
			
		||||
  "bunch_native": {:hex, :bunch_native, "0.5.0", "8ac1536789a597599c10b652e0b526d8833348c19e4739a0759a2bedfd924e63", [:mix], [{:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "24190c760e32b23b36edeb2dc4852515c7c5b3b8675b1a864e0715bdd1c8f80d"},
 | 
			
		||||
  "bundlex": {:hex, :bundlex, "1.5.4", "3726acd463f4d31894a59bbc177c17f3b574634a524212f13469f41c4834a1d9", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:elixir_uuid, "~> 1.2", [hex: :elixir_uuid, repo: "hexpm", optional: false]}, {:qex, "~> 0.5", [hex: :qex, repo: "hexpm", optional: false]}, {:req, ">= 0.4.0", [hex: :req, repo: "hexpm", optional: false]}, {:zarex, "~> 1.0", [hex: :zarex, repo: "hexpm", optional: false]}], "hexpm", "e745726606a560275182a8ac1c8ebd5e11a659bb7460d8abf30f397e59b4c5d2"},
 | 
			
		||||
  "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
 | 
			
		||||
  "castore": {:hex, :castore, "1.0.11", "4bbd584741601eb658007339ea730b082cc61f3554cf2e8f39bf693a11b49073", [:mix], [], "hexpm", "e03990b4db988df56262852f20de0f659871c35154691427a5047f4967a16a62"},
 | 
			
		||||
  "castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"},
 | 
			
		||||
  "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"},
 | 
			
		||||
  "certifi": {:hex, :certifi, "2.14.0", "ed3bef654e69cde5e6c022df8070a579a79e8ba2368a00acf3d75b82d9aceeed", [:rebar3], [], "hexpm", "ea59d87ef89da429b8e905264fdec3419f84f2215bb3d81e07a18aac919026c3"},
 | 
			
		||||
  "coerce": {:hex, :coerce, "1.0.1", "211c27386315dc2894ac11bc1f413a0e38505d808153367bd5c6e75a4003d096", [:mix], [], "hexpm", "b44a691700f7a1a15b4b7e2ff1fa30bebd669929ac8aa43cffe9e2f8bf051cf1"},
 | 
			
		||||
@ -25,7 +25,7 @@
 | 
			
		||||
  "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"},
 | 
			
		||||
  "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
 | 
			
		||||
  "elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"},
 | 
			
		||||
  "esbuild": {:hex, :esbuild, "0.8.2", "5f379dfa383ef482b738e7771daf238b2d1cfb0222bef9d3b20d4c8f06c7a7ac", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "558a8a08ed78eb820efbfda1de196569d8bfa9b51e8371a1934fbb31345feda7"},
 | 
			
		||||
  "esbuild": {:hex, :esbuild, "0.9.0", "f043eeaca4932ca8e16e5429aebd90f7766f31ac160a25cbd9befe84f2bc068f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b415027f71d5ab57ef2be844b2a10d0c1b5a492d431727f43937adce22ba45ae"},
 | 
			
		||||
  "ex_aws": {:hex, :ex_aws, "2.5.8", "0393cfbc5e4a9e7017845451a015d836a670397100aa4c86901980e2a2c5f7d4", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.3", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8f79777b7932168956c8cc3a6db41f5783aa816eb50de356aed3165a71e5f8c3"},
 | 
			
		||||
  "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.6", "d135983bbd8b6df6350dfd83999437725527c1bea151e5055760bfc9b2d17c20", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "9874e12847e469ca2f13a5689be04e546c16f63caf6380870b7f25bf7cb98875"},
 | 
			
		||||
  "ex_m3u8": {:hex, :ex_m3u8, "0.14.2", "3eb17f936e2ca2fdcde11664f3a543e75a94814d928098e050bda5b1e149c021", [:mix], [{:nimble_parsec, "~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.3.0", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "d2a1fb4382a521cce7f966502ecce6187f286ca2852dbb0dcc25dea72f8ba039"},
 | 
			
		||||
@ -109,7 +109,6 @@
 | 
			
		||||
  "superstreamer_player": {:git, "https://github.com/superstreamerapp/superstreamer.git", "9e868acede851f396b3db98fb9799ab4bf712b02", [sparse: "packages/player"]},
 | 
			
		||||
  "sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"},
 | 
			
		||||
  "swoosh": {:hex, :swoosh, "1.17.6", "27ff070f96246e35b7105ab1c52b2b689f523a3cb83ed9faadb2f33bd653ccba", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9798f3e72165f40c950f6762c06dab68afcdcf616138fc4a07965c09c250e1e2"},
 | 
			
		||||
  "tailwind": {:hex, :tailwind, "0.2.4", "5706ec47182d4e7045901302bf3a333e80f3d1af65c442ba9a9eed152fb26c2e", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "c6e4a82b8727bab593700c998a4d98cf3d8025678bfde059aed71d0000c3e463"},
 | 
			
		||||
  "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
 | 
			
		||||
  "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
 | 
			
		||||
  "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},
 | 
			
		||||
 | 
			
		||||
@ -3,10 +3,14 @@ defmodule :"Elixir.Bright.Repo.Migrations.AddXPosts" do
 | 
			
		||||
 | 
			
		||||
  def change do
 | 
			
		||||
    create table(:x_posts) do
 | 
			
		||||
      add :raw, :string, null: false                # Raw content of the tweet
 | 
			
		||||
      add :url, :string, null: false                # URL of the tweet
 | 
			
		||||
      add :date, :utc_datetime, null: false         # Date and time of the tweet
 | 
			
		||||
      add :is_invitation, :boolean, default: false  # Whether the tweet contains an invite link
 | 
			
		||||
      # Raw content of the tweet
 | 
			
		||||
      add :raw, :string, null: false
 | 
			
		||||
      # URL of the tweet
 | 
			
		||||
      add :url, :string, null: false
 | 
			
		||||
      # Date and time of the tweet
 | 
			
		||||
      add :date, :utc_datetime, null: false
 | 
			
		||||
      # Whether the tweet contains an invite link
 | 
			
		||||
      add :is_invitation, :boolean, default: false
 | 
			
		||||
 | 
			
		||||
      add :vtuber_id, references(:vtubers, on_delete: :delete_all), null: false
 | 
			
		||||
 | 
			
		||||
@ -19,5 +23,4 @@ defmodule :"Elixir.Bright.Repo.Migrations.AddXPosts" do
 | 
			
		||||
    # Add an index on the `date` field for sorting and filtering
 | 
			
		||||
    create index(:x_posts, [:date])
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,16 @@
 | 
			
		||||
defmodule Bright.Repo.Migrations.CreateUploads do
 | 
			
		||||
  use Ecto.Migration
 | 
			
		||||
 | 
			
		||||
  def change do
 | 
			
		||||
    create table(:uploads) do
 | 
			
		||||
      add :filename, :string
 | 
			
		||||
      add :size, :integer
 | 
			
		||||
      add :content_type, :string
 | 
			
		||||
      add :user_id, references(:users, on_delete: :nothing)
 | 
			
		||||
 | 
			
		||||
      timestamps(type: :utc_datetime)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    create index(:uploads, [:user_id])
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@ -0,0 +1,18 @@
 | 
			
		||||
defmodule Bright.Repo.Migrations.CreateUploads do
 | 
			
		||||
  use Ecto.Migration
 | 
			
		||||
 | 
			
		||||
  def change do
 | 
			
		||||
    create table(:uploads) do
 | 
			
		||||
      add :filename, :string
 | 
			
		||||
      add :size, :integer
 | 
			
		||||
      add :content_type, :string
 | 
			
		||||
      add :user_id, references(:users, on_delete: :nothing)
 | 
			
		||||
      add :vod, references(:vods, on_delete: :nothing)
 | 
			
		||||
 | 
			
		||||
      timestamps(type: :utc_datetime)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    create index(:uploads, [:user_id])
 | 
			
		||||
    create index(:uploads, [:vod])
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 323 B  | 
@ -3,6 +3,7 @@ defmodule Bright.ImagesTest do
 | 
			
		||||
 | 
			
		||||
  require Logger
 | 
			
		||||
  alias Bright.Images
 | 
			
		||||
  alias Bright.Utils
 | 
			
		||||
 | 
			
		||||
  @test_mp4_fixture "./test/fixtures/SampleVideo_1280x720_1mb.mp4"
 | 
			
		||||
  @test_ts_fixture "./test/fixtures/test-fixture.ts"
 | 
			
		||||
@ -24,13 +25,7 @@ defmodule Bright.ImagesTest do
 | 
			
		||||
 | 
			
		||||
      basename = "thumb.jpg"
 | 
			
		||||
 | 
			
		||||
      random_string =
 | 
			
		||||
        for _ <- 1..12,
 | 
			
		||||
            into: "",
 | 
			
		||||
            do:
 | 
			
		||||
              <<Enum.random(~c"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")>>
 | 
			
		||||
 | 
			
		||||
      output_file = "/tmp/#{random_string}-#{basename}"
 | 
			
		||||
      output_file = "/tmp/#{Utils.random_string()}-#{basename}"
 | 
			
		||||
 | 
			
		||||
      Logger.debug(
 | 
			
		||||
        "output_file=#{inspect(output_file)} @test_mp4_fixture=#{inspect(@test_mp4_fixture)}"
 | 
			
		||||
 | 
			
		||||
@ -56,7 +56,7 @@ defmodule Bright.ProcessPostsTest do
 | 
			
		||||
      Socials.create_x_post(%{
 | 
			
		||||
        raw: "I'm going live! fansly.com/fakename <3",
 | 
			
		||||
        url: "https://x.com/fakename/status/283498235",
 | 
			
		||||
        date: DateTime.utc_now(:second),
 | 
			
		||||
        date: ~U[2025-03-18T13:25:39.000Z],
 | 
			
		||||
        processed_at: nil,
 | 
			
		||||
        vtuber_id: vtuber.id
 | 
			
		||||
      }),
 | 
			
		||||
@ -64,7 +64,7 @@ defmodule Bright.ProcessPostsTest do
 | 
			
		||||
      Socials.create_x_post(%{
 | 
			
		||||
        raw: "gm! tiem for sex breakfast https://onlyfans.com/fakename",
 | 
			
		||||
        url: "https://x.com/fakename/status/283498234",
 | 
			
		||||
        date: DateTime.utc_now(:second),
 | 
			
		||||
        date: ~U[2025-03-18T12:00:00.000Z],
 | 
			
		||||
        processed_at: nil,
 | 
			
		||||
        vtuber_id: vtuber.id
 | 
			
		||||
      }),
 | 
			
		||||
@ -72,7 +72,7 @@ defmodule Bright.ProcessPostsTest do
 | 
			
		||||
      Socials.create_x_post(%{
 | 
			
		||||
        raw: "ero strim rn http://chaturbate.com/fakename",
 | 
			
		||||
        url: "https://x.com/fakename/status/283498232",
 | 
			
		||||
        date: DateTime.utc_now(:second),
 | 
			
		||||
        date: ~U[2025-03-18T10:02:49.000Z],
 | 
			
		||||
        processed_at: nil,
 | 
			
		||||
        vtuber_id: vtuber.id
 | 
			
		||||
      }),
 | 
			
		||||
@ -80,7 +80,7 @@ defmodule Bright.ProcessPostsTest do
 | 
			
		||||
      Socials.create_x_post(%{
 | 
			
		||||
        raw: "Join NOW for some fun! https://example.buzz",
 | 
			
		||||
        url: "https://x.com/fakename/status/394848232",
 | 
			
		||||
        date: DateTime.utc_now(:second),
 | 
			
		||||
        date: ~U[2025-03-18T13:00:03.000Z],
 | 
			
		||||
        processed_at: nil,
 | 
			
		||||
        vtuber_id: vtuber.id
 | 
			
		||||
      }),
 | 
			
		||||
@ -89,7 +89,7 @@ defmodule Bright.ProcessPostsTest do
 | 
			
		||||
      Socials.create_x_post(%{
 | 
			
		||||
        raw: "Let's play a game http://twitch.tv/fakename",
 | 
			
		||||
        url: "https://x.com/fakename/status/283498343",
 | 
			
		||||
        date: DateTime.utc_now(:second),
 | 
			
		||||
        date: ~U[2025-03-18T03:19:23.000Z],
 | 
			
		||||
        processed_at: nil,
 | 
			
		||||
        vtuber_id: vtuber.id
 | 
			
		||||
      }),
 | 
			
		||||
@ -98,7 +98,7 @@ defmodule Bright.ProcessPostsTest do
 | 
			
		||||
        raw:
 | 
			
		||||
          "Be sure to follow me on my socials http://chaturbate.com/fakename http://twitch.tv/fakename http://onlyfans.com/fakename http://linktree.com/fakename",
 | 
			
		||||
        url: "https://x.com/fakename/status/283498349",
 | 
			
		||||
        date: DateTime.utc_now(:second),
 | 
			
		||||
        date: ~U[2025-03-18T14:31:30.000Z],
 | 
			
		||||
        processed_at: nil,
 | 
			
		||||
        vtuber_id: vtuber.id
 | 
			
		||||
      })
 | 
			
		||||
@ -114,8 +114,7 @@ defmodule Bright.ProcessPostsTest do
 | 
			
		||||
    test "create a stream for each post containing an invite link" do
 | 
			
		||||
      {:ok, _} = perform_job(Bright.ObanWorkers.ProcessPosts, %{})
 | 
			
		||||
      streams = Repo.all(Stream)
 | 
			
		||||
 | 
			
		||||
      assert length(streams) == 4
 | 
			
		||||
      assert length(streams) === 4
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    @tag :integration
 | 
			
		||||
 | 
			
		||||
@ -36,7 +36,11 @@ defmodule Bright.ObanWorkers.ProcessVodTest do
 | 
			
		||||
    test "schedule ProcessVod when a new vod is created" do
 | 
			
		||||
      stream = stream_fixture()
 | 
			
		||||
 | 
			
		||||
      vod_fixture(%{thumbnail_url: nil, stream_id: stream.id, origin_temp_input_url: @example_url})
 | 
			
		||||
      vod_fixture(%{
 | 
			
		||||
        thumbnail_url: nil,
 | 
			
		||||
        stream_id: stream.id,
 | 
			
		||||
        origin_temp_input_url: @example_url
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      assert_enqueued(worker: ProcessVod, queue: :default)
 | 
			
		||||
    end
 | 
			
		||||
@ -45,7 +49,11 @@ defmodule Bright.ObanWorkers.ProcessVodTest do
 | 
			
		||||
    test "schedule CreateThumbnail when thumbnail_url is nil" do
 | 
			
		||||
      stream = stream_fixture()
 | 
			
		||||
 | 
			
		||||
      vod_fixture(%{thumbnail_url: nil, stream_id: stream.id, origin_temp_input_url: @example_url})
 | 
			
		||||
      vod_fixture(%{
 | 
			
		||||
        thumbnail_url: nil,
 | 
			
		||||
        stream_id: stream.id,
 | 
			
		||||
        origin_temp_input_url: @example_url
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      assert_enqueued(worker: ProcessVod, queue: :default)
 | 
			
		||||
      # ProcessVod is what queues CreateThumbnail so we need to make it run
 | 
			
		||||
 | 
			
		||||
@ -95,4 +95,65 @@ defmodule Bright.PlatformsTest do
 | 
			
		||||
      assert not Platforms.contains_platform?([], [platformA])
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "platform_aliases" do
 | 
			
		||||
    alias Bright.Platforms.PlatformAlias
 | 
			
		||||
 | 
			
		||||
    import Bright.PlatformsFixtures
 | 
			
		||||
 | 
			
		||||
    @invalid_attrs %{url: nil}
 | 
			
		||||
 | 
			
		||||
    test "list_platform_aliases/0 returns all platform_aliases" do
 | 
			
		||||
      platform_alias = platform_alias_fixture()
 | 
			
		||||
      assert Platforms.list_platform_aliases() == [platform_alias]
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "get_platform_alias!/1 returns the platform_alias with given id" do
 | 
			
		||||
      platform_alias = platform_alias_fixture()
 | 
			
		||||
      assert Platforms.get_platform_alias!(platform_alias.id) == platform_alias
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "create_platform_alias/1 with valid data creates a platform_alias" do
 | 
			
		||||
      valid_attrs = %{url: "some url"}
 | 
			
		||||
 | 
			
		||||
      assert {:ok, %PlatformAlias{} = platform_alias} =
 | 
			
		||||
               Platforms.create_platform_alias(valid_attrs)
 | 
			
		||||
 | 
			
		||||
      assert platform_alias.url == "some url"
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "create_platform_alias/1 with invalid data returns error changeset" do
 | 
			
		||||
      assert {:error, %Ecto.Changeset{}} = Platforms.create_platform_alias(@invalid_attrs)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "update_platform_alias/2 with valid data updates the platform_alias" do
 | 
			
		||||
      platform_alias = platform_alias_fixture()
 | 
			
		||||
      update_attrs = %{url: "some updated url"}
 | 
			
		||||
 | 
			
		||||
      assert {:ok, %PlatformAlias{} = platform_alias} =
 | 
			
		||||
               Platforms.update_platform_alias(platform_alias, update_attrs)
 | 
			
		||||
 | 
			
		||||
      assert platform_alias.url == "some updated url"
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "update_platform_alias/2 with invalid data returns error changeset" do
 | 
			
		||||
      platform_alias = platform_alias_fixture()
 | 
			
		||||
 | 
			
		||||
      assert {:error, %Ecto.Changeset{}} =
 | 
			
		||||
               Platforms.update_platform_alias(platform_alias, @invalid_attrs)
 | 
			
		||||
 | 
			
		||||
      assert platform_alias == Platforms.get_platform_alias!(platform_alias.id)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "delete_platform_alias/1 deletes the platform_alias" do
 | 
			
		||||
      platform_alias = platform_alias_fixture()
 | 
			
		||||
      assert {:ok, %PlatformAlias{}} = Platforms.delete_platform_alias(platform_alias)
 | 
			
		||||
      assert_raise Ecto.NoResultsError, fn -> Platforms.get_platform_alias!(platform_alias.id) end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "change_platform_alias/1 returns a platform_alias changeset" do
 | 
			
		||||
      platform_alias = platform_alias_fixture()
 | 
			
		||||
      assert %Ecto.Changeset{} = Platforms.change_platform_alias(platform_alias)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
@ -449,6 +449,25 @@ defmodule Bright.XPostTest do
 | 
			
		||||
      assert XPost.is_nsfw_live_announcement?(x_post, known_platforms)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "should return false when receiving an XPost mentioning a Fansly /post", %{
 | 
			
		||||
      known_platforms: known_platforms
 | 
			
		||||
    } do
 | 
			
		||||
      vtuber = VtubersFixtures.el_xox_fixture()
 | 
			
		||||
 | 
			
		||||
      x_post =
 | 
			
		||||
        %XPost{
 | 
			
		||||
          url: "https://x.com/el_XoX34/status/1899913287808262522",
 | 
			
		||||
          raw:
 | 
			
		||||
            "Being a good girl and taking all of your big cock inside me 🍆💦 Subscribe Tier 2 Cummy Bunny 🐰💙 or pay $15 to see 5 spicy photos!! ▶️ fansly.com/post/754877662",
 | 
			
		||||
          date: ~U[2025-03-12T20:00:01Z],
 | 
			
		||||
          processed_at: nil,
 | 
			
		||||
          vtuber_id: vtuber.id
 | 
			
		||||
        }
 | 
			
		||||
        |> Repo.preload(:vtuber)
 | 
			
		||||
 | 
			
		||||
      assert not XPost.is_nsfw_live_announcement?(x_post, known_platforms)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "should return false when the XPost is a retweet", %{
 | 
			
		||||
      known_platforms: known_platforms
 | 
			
		||||
    } do
 | 
			
		||||
 | 
			
		||||
@ -188,4 +188,62 @@ defmodule Bright.StreamsTest do
 | 
			
		||||
      # assert_received {:progress, %{stage: :generating_thumbnail, done: 1, total: 1}}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "uploads" do
 | 
			
		||||
    alias Bright.Streams.Upload
 | 
			
		||||
 | 
			
		||||
    import Bright.StreamsFixtures
 | 
			
		||||
 | 
			
		||||
    @invalid_attrs %{size: nil, filename: nil, content_type: nil}
 | 
			
		||||
 | 
			
		||||
    test "list_uploads/0 returns all uploads" do
 | 
			
		||||
      upload = upload_fixture()
 | 
			
		||||
      assert Streams.list_uploads() == [upload]
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "get_upload!/1 returns the upload with given id" do
 | 
			
		||||
      upload = upload_fixture()
 | 
			
		||||
      assert Streams.get_upload!(upload.id) == upload
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "create_upload/1 with valid data creates a upload" do
 | 
			
		||||
      valid_attrs = %{size: 42, filename: "some filename", content_type: "some content_type"}
 | 
			
		||||
 | 
			
		||||
      assert {:ok, %Upload{} = upload} = Streams.create_upload(valid_attrs)
 | 
			
		||||
      assert upload.size == 42
 | 
			
		||||
      assert upload.filename == "some filename"
 | 
			
		||||
      assert upload.content_type == "some content_type"
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "create_upload/1 with invalid data returns error changeset" do
 | 
			
		||||
      assert {:error, %Ecto.Changeset{}} = Streams.create_upload(@invalid_attrs)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "update_upload/2 with valid data updates the upload" do
 | 
			
		||||
      upload = upload_fixture()
 | 
			
		||||
      update_attrs = %{size: 43, filename: "some updated filename", content_type: "some updated content_type"}
 | 
			
		||||
 | 
			
		||||
      assert {:ok, %Upload{} = upload} = Streams.update_upload(upload, update_attrs)
 | 
			
		||||
      assert upload.size == 43
 | 
			
		||||
      assert upload.filename == "some updated filename"
 | 
			
		||||
      assert upload.content_type == "some updated content_type"
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "update_upload/2 with invalid data returns error changeset" do
 | 
			
		||||
      upload = upload_fixture()
 | 
			
		||||
      assert {:error, %Ecto.Changeset{}} = Streams.update_upload(upload, @invalid_attrs)
 | 
			
		||||
      assert upload == Streams.get_upload!(upload.id)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "delete_upload/1 deletes the upload" do
 | 
			
		||||
      upload = upload_fixture()
 | 
			
		||||
      assert {:ok, %Upload{}} = Streams.delete_upload(upload)
 | 
			
		||||
      assert_raise Ecto.NoResultsError, fn -> Streams.get_upload!(upload.id) end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "change_upload/1 returns a upload changeset" do
 | 
			
		||||
      upload = upload_fixture()
 | 
			
		||||
      assert %Ecto.Changeset{} = Streams.change_upload(upload)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,87 @@
 | 
			
		||||
defmodule BrightWeb.PlatformAliasControllerTest do
 | 
			
		||||
  use BrightWeb.ConnCase
 | 
			
		||||
 | 
			
		||||
  import Bright.PlatformsFixtures
 | 
			
		||||
 | 
			
		||||
  @create_attrs %{url: "some url"}
 | 
			
		||||
  @update_attrs %{url: "some updated url"}
 | 
			
		||||
  @invalid_attrs %{url: nil}
 | 
			
		||||
 | 
			
		||||
  describe "index" do
 | 
			
		||||
    test "lists all platform_aliases", %{conn: conn} do
 | 
			
		||||
      conn = get(conn, ~p"/platform_aliases")
 | 
			
		||||
      assert html_response(conn, 200) =~ "Listing Platform aliases"
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "new platform_alias" do
 | 
			
		||||
    test "renders form", %{conn: conn} do
 | 
			
		||||
      conn = get(conn, ~p"/platform_aliases/new")
 | 
			
		||||
      assert html_response(conn, 200) =~ "New Platform alias"
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "create platform_alias" do
 | 
			
		||||
    test "redirects to show when data is valid", %{conn: conn} do
 | 
			
		||||
      conn = post(conn, ~p"/platform_aliases", platform_alias: @create_attrs)
 | 
			
		||||
 | 
			
		||||
      assert %{id: id} = redirected_params(conn)
 | 
			
		||||
      assert redirected_to(conn) == ~p"/platform_aliases/#{id}"
 | 
			
		||||
 | 
			
		||||
      conn = get(conn, ~p"/platform_aliases/#{id}")
 | 
			
		||||
      assert html_response(conn, 200) =~ "Platform alias #{id}"
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "renders errors when data is invalid", %{conn: conn} do
 | 
			
		||||
      conn = post(conn, ~p"/platform_aliases", platform_alias: @invalid_attrs)
 | 
			
		||||
      assert html_response(conn, 200) =~ "New Platform alias"
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "edit platform_alias" do
 | 
			
		||||
    setup [:create_platform_alias]
 | 
			
		||||
 | 
			
		||||
    test "renders form for editing chosen platform_alias", %{
 | 
			
		||||
      conn: conn,
 | 
			
		||||
      platform_alias: platform_alias
 | 
			
		||||
    } do
 | 
			
		||||
      conn = get(conn, ~p"/platform_aliases/#{platform_alias}/edit")
 | 
			
		||||
      assert html_response(conn, 200) =~ "Edit Platform alias"
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "update platform_alias" do
 | 
			
		||||
    setup [:create_platform_alias]
 | 
			
		||||
 | 
			
		||||
    test "redirects when data is valid", %{conn: conn, platform_alias: platform_alias} do
 | 
			
		||||
      conn = put(conn, ~p"/platform_aliases/#{platform_alias}", platform_alias: @update_attrs)
 | 
			
		||||
      assert redirected_to(conn) == ~p"/platform_aliases/#{platform_alias}"
 | 
			
		||||
 | 
			
		||||
      conn = get(conn, ~p"/platform_aliases/#{platform_alias}")
 | 
			
		||||
      assert html_response(conn, 200) =~ "some updated url"
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "renders errors when data is invalid", %{conn: conn, platform_alias: platform_alias} do
 | 
			
		||||
      conn = put(conn, ~p"/platform_aliases/#{platform_alias}", platform_alias: @invalid_attrs)
 | 
			
		||||
      assert html_response(conn, 200) =~ "Edit Platform alias"
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "delete platform_alias" do
 | 
			
		||||
    setup [:create_platform_alias]
 | 
			
		||||
 | 
			
		||||
    test "deletes chosen platform_alias", %{conn: conn, platform_alias: platform_alias} do
 | 
			
		||||
      conn = delete(conn, ~p"/platform_aliases/#{platform_alias}")
 | 
			
		||||
      assert redirected_to(conn) == ~p"/platform_aliases"
 | 
			
		||||
 | 
			
		||||
      assert_error_sent 404, fn ->
 | 
			
		||||
        get(conn, ~p"/platform_aliases/#{platform_alias}")
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp create_platform_alias(_) do
 | 
			
		||||
    platform_alias = platform_alias_fixture()
 | 
			
		||||
    %{platform_alias: platform_alias}
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@ -0,0 +1,84 @@
 | 
			
		||||
defmodule BrightWeb.UploadControllerTest do
 | 
			
		||||
  use BrightWeb.ConnCase
 | 
			
		||||
 | 
			
		||||
  import Bright.StreamsFixtures
 | 
			
		||||
 | 
			
		||||
  @create_attrs %{size: 42, filename: "some filename", content_type: "some content_type"}
 | 
			
		||||
  @update_attrs %{size: 43, filename: "some updated filename", content_type: "some updated content_type"}
 | 
			
		||||
  @invalid_attrs %{size: nil, filename: nil, content_type: nil}
 | 
			
		||||
 | 
			
		||||
  describe "index" do
 | 
			
		||||
    test "lists all uploads", %{conn: conn} do
 | 
			
		||||
      conn = get(conn, ~p"/uploads")
 | 
			
		||||
      assert html_response(conn, 200) =~ "Listing Uploads"
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "new upload" do
 | 
			
		||||
    test "renders form", %{conn: conn} do
 | 
			
		||||
      conn = get(conn, ~p"/uploads/new")
 | 
			
		||||
      assert html_response(conn, 200) =~ "New Upload"
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "create upload" do
 | 
			
		||||
    test "redirects to show when data is valid", %{conn: conn} do
 | 
			
		||||
      conn = post(conn, ~p"/uploads", upload: @create_attrs)
 | 
			
		||||
 | 
			
		||||
      assert %{id: id} = redirected_params(conn)
 | 
			
		||||
      assert redirected_to(conn) == ~p"/uploads/#{id}"
 | 
			
		||||
 | 
			
		||||
      conn = get(conn, ~p"/uploads/#{id}")
 | 
			
		||||
      assert html_response(conn, 200) =~ "Upload #{id}"
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "renders errors when data is invalid", %{conn: conn} do
 | 
			
		||||
      conn = post(conn, ~p"/uploads", upload: @invalid_attrs)
 | 
			
		||||
      assert html_response(conn, 200) =~ "New Upload"
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "edit upload" do
 | 
			
		||||
    setup [:create_upload]
 | 
			
		||||
 | 
			
		||||
    test "renders form for editing chosen upload", %{conn: conn, upload: upload} do
 | 
			
		||||
      conn = get(conn, ~p"/uploads/#{upload}/edit")
 | 
			
		||||
      assert html_response(conn, 200) =~ "Edit Upload"
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "update upload" do
 | 
			
		||||
    setup [:create_upload]
 | 
			
		||||
 | 
			
		||||
    test "redirects when data is valid", %{conn: conn, upload: upload} do
 | 
			
		||||
      conn = put(conn, ~p"/uploads/#{upload}", upload: @update_attrs)
 | 
			
		||||
      assert redirected_to(conn) == ~p"/uploads/#{upload}"
 | 
			
		||||
 | 
			
		||||
      conn = get(conn, ~p"/uploads/#{upload}")
 | 
			
		||||
      assert html_response(conn, 200) =~ "some updated filename"
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "renders errors when data is invalid", %{conn: conn, upload: upload} do
 | 
			
		||||
      conn = put(conn, ~p"/uploads/#{upload}", upload: @invalid_attrs)
 | 
			
		||||
      assert html_response(conn, 200) =~ "Edit Upload"
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "delete upload" do
 | 
			
		||||
    setup [:create_upload]
 | 
			
		||||
 | 
			
		||||
    test "deletes chosen upload", %{conn: conn, upload: upload} do
 | 
			
		||||
      conn = delete(conn, ~p"/uploads/#{upload}")
 | 
			
		||||
      assert redirected_to(conn) == ~p"/uploads"
 | 
			
		||||
 | 
			
		||||
      assert_error_sent 404, fn ->
 | 
			
		||||
        get(conn, ~p"/uploads/#{upload}")
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp create_upload(_) do
 | 
			
		||||
    upload = upload_fixture()
 | 
			
		||||
    %{upload: upload}
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										113
									
								
								apps/bright/test/bright_web/live/upload_live_test.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								apps/bright/test/bright_web/live/upload_live_test.exs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,113 @@
 | 
			
		||||
defmodule BrightWeb.UploadLiveTest do
 | 
			
		||||
  use BrightWeb.ConnCase
 | 
			
		||||
 | 
			
		||||
  import Phoenix.LiveViewTest
 | 
			
		||||
  import Bright.StreamsFixtures
 | 
			
		||||
 | 
			
		||||
  @create_attrs %{size: 42, filename: "some filename", content_type: "some content_type"}
 | 
			
		||||
  @update_attrs %{size: 43, filename: "some updated filename", content_type: "some updated content_type"}
 | 
			
		||||
  @invalid_attrs %{size: nil, filename: nil, content_type: nil}
 | 
			
		||||
 | 
			
		||||
  defp create_upload(_) do
 | 
			
		||||
    upload = upload_fixture()
 | 
			
		||||
    %{upload: upload}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "Index" do
 | 
			
		||||
    setup [:create_upload]
 | 
			
		||||
 | 
			
		||||
    test "lists all uploads", %{conn: conn, upload: upload} do
 | 
			
		||||
      {:ok, _index_live, html} = live(conn, ~p"/uploads")
 | 
			
		||||
 | 
			
		||||
      assert html =~ "Listing Uploads"
 | 
			
		||||
      assert html =~ upload.filename
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "saves new upload", %{conn: conn} do
 | 
			
		||||
      {:ok, index_live, _html} = live(conn, ~p"/uploads")
 | 
			
		||||
 | 
			
		||||
      assert index_live |> element("a", "New Upload") |> render_click() =~
 | 
			
		||||
               "New Upload"
 | 
			
		||||
 | 
			
		||||
      assert_patch(index_live, ~p"/uploads/new")
 | 
			
		||||
 | 
			
		||||
      assert index_live
 | 
			
		||||
             |> form("#upload-form", upload: @invalid_attrs)
 | 
			
		||||
             |> render_change() =~ "can't be blank"
 | 
			
		||||
 | 
			
		||||
      assert index_live
 | 
			
		||||
             |> form("#upload-form", upload: @create_attrs)
 | 
			
		||||
             |> render_submit()
 | 
			
		||||
 | 
			
		||||
      assert_patch(index_live, ~p"/uploads")
 | 
			
		||||
 | 
			
		||||
      html = render(index_live)
 | 
			
		||||
      assert html =~ "Upload created successfully"
 | 
			
		||||
      assert html =~ "some filename"
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "updates upload in listing", %{conn: conn, upload: upload} do
 | 
			
		||||
      {:ok, index_live, _html} = live(conn, ~p"/uploads")
 | 
			
		||||
 | 
			
		||||
      assert index_live |> element("#uploads-#{upload.id} a", "Edit") |> render_click() =~
 | 
			
		||||
               "Edit Upload"
 | 
			
		||||
 | 
			
		||||
      assert_patch(index_live, ~p"/uploads/#{upload}/edit")
 | 
			
		||||
 | 
			
		||||
      assert index_live
 | 
			
		||||
             |> form("#upload-form", upload: @invalid_attrs)
 | 
			
		||||
             |> render_change() =~ "can't be blank"
 | 
			
		||||
 | 
			
		||||
      assert index_live
 | 
			
		||||
             |> form("#upload-form", upload: @update_attrs)
 | 
			
		||||
             |> render_submit()
 | 
			
		||||
 | 
			
		||||
      assert_patch(index_live, ~p"/uploads")
 | 
			
		||||
 | 
			
		||||
      html = render(index_live)
 | 
			
		||||
      assert html =~ "Upload updated successfully"
 | 
			
		||||
      assert html =~ "some updated filename"
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "deletes upload in listing", %{conn: conn, upload: upload} do
 | 
			
		||||
      {:ok, index_live, _html} = live(conn, ~p"/uploads")
 | 
			
		||||
 | 
			
		||||
      assert index_live |> element("#uploads-#{upload.id} a", "Delete") |> render_click()
 | 
			
		||||
      refute has_element?(index_live, "#uploads-#{upload.id}")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "Show" do
 | 
			
		||||
    setup [:create_upload]
 | 
			
		||||
 | 
			
		||||
    test "displays upload", %{conn: conn, upload: upload} do
 | 
			
		||||
      {:ok, _show_live, html} = live(conn, ~p"/uploads/#{upload}")
 | 
			
		||||
 | 
			
		||||
      assert html =~ "Show Upload"
 | 
			
		||||
      assert html =~ upload.filename
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "updates upload within modal", %{conn: conn, upload: upload} do
 | 
			
		||||
      {:ok, show_live, _html} = live(conn, ~p"/uploads/#{upload}")
 | 
			
		||||
 | 
			
		||||
      assert show_live |> element("a", "Edit") |> render_click() =~
 | 
			
		||||
               "Edit Upload"
 | 
			
		||||
 | 
			
		||||
      assert_patch(show_live, ~p"/uploads/#{upload}/show/edit")
 | 
			
		||||
 | 
			
		||||
      assert show_live
 | 
			
		||||
             |> form("#upload-form", upload: @invalid_attrs)
 | 
			
		||||
             |> render_change() =~ "can't be blank"
 | 
			
		||||
 | 
			
		||||
      assert show_live
 | 
			
		||||
             |> form("#upload-form", upload: @update_attrs)
 | 
			
		||||
             |> render_submit()
 | 
			
		||||
 | 
			
		||||
      assert_patch(show_live, ~p"/uploads/#{upload}")
 | 
			
		||||
 | 
			
		||||
      html = render(show_live)
 | 
			
		||||
      assert html =~ "Upload updated successfully"
 | 
			
		||||
      assert html =~ "some updated filename"
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@ -4,9 +4,6 @@ defmodule Bright.PlatformsFixtures do
 | 
			
		||||
  entities via the `Bright.Platforms` context.
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  alias Bright.Platforms.Platform
 | 
			
		||||
  alias Bright.Platforms.PlatformAlias
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Generate a platform.
 | 
			
		||||
  """
 | 
			
		||||
@ -55,7 +52,7 @@ defmodule Bright.PlatformsFixtures do
 | 
			
		||||
      }
 | 
			
		||||
      |> Bright.Platforms.create_platform()
 | 
			
		||||
 | 
			
		||||
    {:ok, platform_alias} =
 | 
			
		||||
    {:ok, _platform_alias} =
 | 
			
		||||
      %{
 | 
			
		||||
        url: "https://melody.buzz",
 | 
			
		||||
        platform_id: platform.id
 | 
			
		||||
@ -112,4 +109,18 @@ defmodule Bright.PlatformsFixtures do
 | 
			
		||||
 | 
			
		||||
    platform
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Generate a platform_alias.
 | 
			
		||||
  """
 | 
			
		||||
  def platform_alias_fixture(attrs \\ %{}) do
 | 
			
		||||
    {:ok, platform_alias} =
 | 
			
		||||
      attrs
 | 
			
		||||
      |> Enum.into(%{
 | 
			
		||||
        url: "some url"
 | 
			
		||||
      })
 | 
			
		||||
      |> Bright.Platforms.create_platform_alias()
 | 
			
		||||
 | 
			
		||||
    platform_alias
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
@ -36,4 +36,20 @@ defmodule Bright.StreamsFixtures do
 | 
			
		||||
 | 
			
		||||
    vod
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Generate a upload.
 | 
			
		||||
  """
 | 
			
		||||
  def upload_fixture(attrs \\ %{}) do
 | 
			
		||||
    {:ok, upload} =
 | 
			
		||||
      attrs
 | 
			
		||||
      |> Enum.into(%{
 | 
			
		||||
        content_type: "some content_type",
 | 
			
		||||
        filename: "some filename",
 | 
			
		||||
        size: 42
 | 
			
		||||
      })
 | 
			
		||||
      |> Bright.Streams.create_upload()
 | 
			
		||||
 | 
			
		||||
    upload
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,4 @@
 | 
			
		||||
defmodule Bright.XPostsFixtures do
 | 
			
		||||
  alias Bright.Socials.XPost
 | 
			
		||||
  alias Bright.Socials
 | 
			
		||||
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
@ -149,7 +148,7 @@ defmodule Bright.XPostsFixtures do
 | 
			
		||||
    {:ok, x_post} =
 | 
			
		||||
      attrs
 | 
			
		||||
      |> Enum.into(defaults)
 | 
			
		||||
      |> XPost.create_x_post()
 | 
			
		||||
      |> Socials.create_x_post()
 | 
			
		||||
 | 
			
		||||
    x_post
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
@ -64,7 +64,15 @@ services:
 | 
			
		||||
  #       # - path: /home/cj/Documents/ueberauth_patreon
 | 
			
		||||
  #       #   action: sync
 | 
			
		||||
  #       #   target: /app/contrib/ueberauth_patreon
 | 
			
		||||
 | 
			
		||||
  # uppy:
 | 
			
		||||
  #   image: transloadit/companion:latest
 | 
			
		||||
  #   container_name: uppy-companion
 | 
			
		||||
  #   env_file:
 | 
			
		||||
  #     - .env.development
 | 
			
		||||
  #   volumes:
 | 
			
		||||
  #     - uppy_data:/mnt/uppy-server-data
 | 
			
		||||
  #   ports:
 | 
			
		||||
  #     - '3020:3020'
 | 
			
		||||
 | 
			
		||||
  db:
 | 
			
		||||
    image: postgres:15
 | 
			
		||||
@ -96,3 +104,4 @@ services:
 | 
			
		||||
volumes:
 | 
			
		||||
  pg_data:
 | 
			
		||||
  cache:
 | 
			
		||||
  uppy_data:
 | 
			
		||||
 | 
			
		||||
@ -91,6 +91,7 @@ resource "vultr_reserved_ip" "futureporn_tracker_ip" {
 | 
			
		||||
  ip_type = "v4"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Virtual Private Cloud for connecting many VPS together on a private network
 | 
			
		||||
# We use this network connection for app<->db comms.
 | 
			
		||||
resource "vultr_vpc2" "futureporn_vpc2" {
 | 
			
		||||
@ -155,6 +156,29 @@ resource "vultr_instance" "capture_vps" {
 | 
			
		||||
  user_data       = base64encode(var.vps_user_data)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# vultr instance with a GPU. experimental.
 | 
			
		||||
# resource "vultr_instance" "capture_vps" {
 | 
			
		||||
#   count           = 0
 | 
			
		||||
#   hostname        = "fp-cap-${count.index}"
 | 
			
		||||
#   plan            = "vcg-a16-2c-8g-2vram"
 | 
			
		||||
#   region          = "ord"
 | 
			
		||||
#   backups         = "disabled"
 | 
			
		||||
#   ddos_protection = "false"
 | 
			
		||||
#   # os_id           = 1743
 | 
			
		||||
#   image_id = "ubuntu-xfce"
 | 
			
		||||
#   app_variables = {
 | 
			
		||||
#     desktopuser = "cj_clippy"
 | 
			
		||||
#   }
 | 
			
		||||
#   enable_ipv6 = true
 | 
			
		||||
#   vpc2_ids    = [vultr_vpc2.futureporn_vpc2.id]
 | 
			
		||||
#   label       = "fp capture ${count.index}"
 | 
			
		||||
#   tags        = ["futureporn", "capture"]
 | 
			
		||||
#   ssh_key_ids = [local.envs.VULTR_SSH_KEY_ID]
 | 
			
		||||
#   user_data   = base64encode(var.vps_user_data)
 | 
			
		||||
# }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
resource "vultr_instance" "database" {
 | 
			
		||||
  count    = 1
 | 
			
		||||
  hostname = "fp-db-${count.index}"
 | 
			
		||||
@ -254,6 +278,7 @@ resource "ansible_group" "capture" {
 | 
			
		||||
  name = "capture"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
resource "ansible_group" "bright" {
 | 
			
		||||
  name = "bright"
 | 
			
		||||
}
 | 
			
		||||
@ -277,7 +302,7 @@ resource "ansible_group" "futureporn" {
 | 
			
		||||
    "database",
 | 
			
		||||
    "capture",
 | 
			
		||||
    "bright",
 | 
			
		||||
    "tracker",
 | 
			
		||||
    "tracker"
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user