First UI Pass (#15)

* Added scratchpad dir

* Installed Alpine

* [WIP] began integrating Tailwind and accompanying theme

* [WIP] Set up basic views for sources

* Adds new UI to media profiles page

* Removes unneeded view
This commit is contained in:
Kieran 2024-02-08 19:03:11 -08:00 committed by GitHub
parent 9e4fbfa35d
commit e1565ad22f
76 changed files with 1029 additions and 297 deletions

1
.gitignore vendored
View file

@ -40,3 +40,4 @@ npm-debug.log
.env
.DS_Store
scratchpad.md
/scratchpad/

View file

@ -2,4 +2,30 @@
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
@import './satoshi';
/* This file is for your main application CSS */
[x-cloak] {
display: none !important;
}
@layer base {
body {
@apply relative z-1 bg-whiten font-satoshi text-base font-normal text-body;
}
}
@layer components {
}
@layer utilities {
/* Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
}

102
assets/css/satoshi.css Normal file
View file

@ -0,0 +1,102 @@
/**
* @license
*
* Font Family: Satoshi
* Designed by: Deni Anggara
* URL: https://www.fontshare.com/fonts/satoshi
* © 2023 Indian Type Foundry
*
* Font Styles:
* Satoshi Light
* Satoshi Light Italic
* Satoshi Regular
* Satoshi Italic
* Satoshi Medium
* Satoshi Medium Italic
* Satoshi Bold
* Satoshi Bold Italic
* Satoshi Black
* Satoshi Black Italic
*
*/
@font-face {
font-family: 'Satoshi';
src: url('/fonts/satoshi/Satoshi-Light.woff2'), url('/fonts/satoshi/Satoshi-Light.woff'),
url('/fonts/satoshi/Satoshi-Light.ttf');
font-weight: 300;
font-display: swap;
font-style: normal;
}
@font-face {
font-family: 'Satoshi';
src: url('/fonts/satoshi/Satoshi-LightItalic.woff2'),
url('/fonts/satoshi/Satoshi-LightItalic.woff'), url('/fonts/satoshi/Satoshi-LightItalic.ttf');
font-weight: 300;
font-display: swap;
font-style: italic;
}
@font-face {
font-family: 'Satoshi';
src: url('/fonts/satoshi/Satoshi-Regular.woff2'), url('/fonts/satoshi/Satoshi-Regular.woff'),
url('/fonts/satoshi/Satoshi-Regular.ttf');
font-weight: 400;
font-display: swap;
font-style: normal;
}
@font-face {
font-family: 'Satoshi';
src: url('/fonts/satoshi/Satoshi-Italic.woff2'), url('/fonts/satoshi/Satoshi-Italic.woff'),
url('/fonts/satoshi/Satoshi-Italic.ttf');
font-weight: 400;
font-display: swap;
font-style: italic;
}
@font-face {
font-family: 'Satoshi';
src: url('/fonts/satoshi/Satoshi-Medium.woff2'), url('/fonts/satoshi/Satoshi-Medium.woff'),
url('/fonts/satoshi/Satoshi-Medium.ttf');
font-weight: 500;
font-display: swap;
font-style: normal;
}
@font-face {
font-family: 'Satoshi';
src: url('/fonts/satoshi/Satoshi-MediumItalic.woff2'),
url('/fonts/satoshi/Satoshi-MediumItalic.woff'), url('/fonts/satoshi/Satoshi-MediumItalic.ttf');
font-weight: 500;
font-display: swap;
font-style: italic;
}
@font-face {
font-family: 'Satoshi';
src: url('/fonts/satoshi/Satoshi-Bold.woff2'), url('/fonts/satoshi/Satoshi-Bold.woff'),
url('/fonts/satoshi/Satoshi-Bold.ttf');
font-weight: 700;
font-display: swap;
font-style: normal;
}
@font-face {
font-family: 'Satoshi';
src: url('/fonts/satoshi/Satoshi-BoldItalic.woff2'), url('/fonts/satoshi/Satoshi-BoldItalic.woff'),
url('/fonts/satoshi/Satoshi-BoldItalic.ttf');
font-weight: 700;
font-display: swap;
font-style: italic;
}
@font-face {
font-family: 'Satoshi';
src: url('/fonts/satoshi/Satoshi-BlackItalic.woff2'),
url('/fonts/satoshi/Satoshi-BlackItalic.woff'), url('/fonts/satoshi/Satoshi-BlackItalic.ttf');
font-weight: 900;
font-display: swap;
font-style: italic;
}

View file

@ -21,10 +21,21 @@ import 'phoenix_html'
import { Socket } from 'phoenix'
import { LiveSocket } from 'phoenix_live_view'
import topbar from '../vendor/topbar'
import Alpine from 'alpinejs'
window.Alpine = Alpine
Alpine.start()
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute('content')
let liveSocket = new LiveSocket('/live', Socket, {
params: { _csrf_token: csrfToken }
params: { _csrf_token: csrfToken },
dom: {
onBeforeElUpdated(from, to) {
if (from._x_dataStack) {
window.Alpine.clone(from, to)
}
}
}
})
// Show progress bar on live navigation and form submits

View file

@ -1,3 +1,6 @@
{
"description": "Use this one for actual JS deps used in-app"
"description": "Use this one for actual JS deps used in-app",
"dependencies": {
"alpinejs": "^3.13.5"
}
}

View file

@ -2,15 +2,288 @@
// https://tailwindcss.com/docs/configuration
const plugin = require('tailwindcss/plugin')
const defaultTheme = require('tailwindcss/defaultTheme')
const fs = require('fs')
const path = require('path')
module.exports = {
content: ['./js/**/*.js', '../lib/pinchflat_web.ex', '../lib/pinchflat_web/**/*.*ex'],
darkMode: 'class',
theme: {
fontFamily: {
satoshi: ['Satoshi', 'sans-serif']
},
screens: {
'2xsm': '375px',
xsm: '425px',
'3xl': '2000px',
...defaultTheme.screens
},
extend: {
colors: {
brand: '#FD4F00'
current: 'currentColor',
transparent: 'transparent',
white: '#FFFFFF',
black: '#1C2434',
'black-2': '#010101',
body: '#64748B',
bodydark: '#AEB7C0',
bodydark1: '#DEE4EE',
bodydark2: '#8A99AF',
primary: '#3C50E0',
secondary: '#80CAEE',
stroke: '#E2E8F0',
gray: '#EFF4FB',
graydark: '#333A48',
'gray-2': '#F7F9FC',
'gray-3': '#FAFAFA',
whiten: '#F1F5F9',
whiter: '#F5F7FD',
boxdark: '#24303F',
'boxdark-2': '#1A222C',
strokedark: '#2E3A47',
'form-strokedark': '#3d4d60',
'form-input': '#1d2a39',
'meta-1': '#DC3545',
'meta-2': '#EFF2F7',
'meta-3': '#10B981',
'meta-4': '#313D4A',
'meta-5': '#259AE6',
'meta-6': '#FFBA00',
'meta-7': '#FF6766',
'meta-8': '#F0950C',
'meta-9': '#E5E7EB',
success: '#219653',
danger: '#D34053',
warning: '#FFA70B'
},
fontSize: {
'title-xxl': ['44px', '55px'],
'title-xl': ['36px', '45px'],
'title-xl2': ['33px', '45px'],
'title-lg': ['28px', '35px'],
'title-md': ['24px', '30px'],
'title-md2': ['26px', '30px'],
'title-sm': ['20px', '26px'],
'title-xsm': ['18px', '24px']
},
spacing: {
4.5: '1.125rem',
5.5: '1.375rem',
6.5: '1.625rem',
7.5: '1.875rem',
8.5: '2.125rem',
9.5: '2.375rem',
10.5: '2.625rem',
11: '2.75rem',
11.5: '2.875rem',
12.5: '3.125rem',
13: '3.25rem',
13.5: '3.375rem',
14: '3.5rem',
14.5: '3.625rem',
15: '3.75rem',
15.5: '3.875rem',
16: '4rem',
16.5: '4.125rem',
17: '4.25rem',
17.5: '4.375rem',
18: '4.5rem',
18.5: '4.625rem',
19: '4.75rem',
19.5: '4.875rem',
21: '5.25rem',
21.5: '5.375rem',
22: '5.5rem',
22.5: '5.625rem',
24.5: '6.125rem',
25: '6.25rem',
25.5: '6.375rem',
26: '6.5rem',
27: '6.75rem',
27.5: '6.875rem',
29: '7.25rem',
29.5: '7.375rem',
30: '7.5rem',
31: '7.75rem',
32.5: '8.125rem',
34: '8.5rem',
34.5: '8.625rem',
35: '8.75rem',
36.5: '9.125rem',
37.5: '9.375rem',
39: '9.75rem',
39.5: '9.875rem',
40: '10rem',
42.5: '10.625rem',
44: '11rem',
45: '11.25rem',
46: '11.5rem',
47.5: '11.875rem',
49: '12.25rem',
50: '12.5rem',
52: '13rem',
52.5: '13.125rem',
54: '13.5rem',
54.5: '13.625rem',
55: '13.75rem',
55.5: '13.875rem',
59: '14.75rem',
60: '15rem',
62.5: '15.625rem',
65: '16.25rem',
67: '16.75rem',
67.5: '16.875rem',
70: '17.5rem',
72.5: '18.125rem',
73: '18.25rem',
75: '18.75rem',
90: '22.5rem',
94: '23.5rem',
95: '23.75rem',
100: '25rem',
115: '28.75rem',
125: '31.25rem',
132.5: '33.125rem',
150: '37.5rem',
171.5: '42.875rem',
180: '45rem',
187.5: '46.875rem',
203: '50.75rem',
230: '57.5rem',
242.5: '60.625rem'
},
maxWidth: {
2.5: '0.625rem',
3: '0.75rem',
4: '1rem',
11: '2.75rem',
13: '3.25rem',
14: '3.5rem',
15: '3.75rem',
22.5: '5.625rem',
25: '6.25rem',
30: '7.5rem',
34: '8.5rem',
35: '8.75rem',
40: '10rem',
42.5: '10.625rem',
44: '11rem',
45: '11.25rem',
60: '15rem',
70: '17.5rem',
90: '22.5rem',
94: '23.5rem',
125: '31.25rem',
132.5: '33.125rem',
142.5: '35.625rem',
150: '37.5rem',
180: '45rem',
203: '50.75rem',
230: '57.5rem',
242.5: '60.625rem',
270: '67.5rem',
280: '70rem',
292.5: '73.125rem'
},
maxHeight: {
35: '8.75rem',
70: '17.5rem',
90: '22.5rem',
550: '34.375rem',
300: '18.75rem'
},
minWidth: {
22.5: '5.625rem',
42.5: '10.625rem',
47.5: '11.875rem',
75: '18.75rem'
},
zIndex: {
999999: '999999',
99999: '99999',
9999: '9999',
999: '999',
99: '99',
9: '9',
1: '1'
},
opacity: {
65: '.65'
},
transitionProperty: { width: 'width', stroke: 'stroke' },
borderWidth: {
6: '6px'
},
boxShadow: {
default: '0px 8px 13px -3px rgba(0, 0, 0, 0.07)',
card: '0px 1px 3px rgba(0, 0, 0, 0.12)',
'card-2': '0px 1px 2px rgba(0, 0, 0, 0.05)',
switcher:
'0px 2px 4px rgba(0, 0, 0, 0.2), inset 0px 2px 2px #FFFFFF, inset 0px -1px 1px rgba(0, 0, 0, 0.1)',
'switch-1': '0px 0px 5px rgba(0, 0, 0, 0.15)',
1: '0px 1px 3px rgba(0, 0, 0, 0.08)',
2: '0px 1px 4px rgba(0, 0, 0, 0.12)',
3: '0px 1px 5px rgba(0, 0, 0, 0.14)',
4: '0px 4px 10px rgba(0, 0, 0, 0.12)',
5: '0px 1px 1px rgba(0, 0, 0, 0.15)',
6: '0px 3px 15px rgba(0, 0, 0, 0.1)',
7: '-5px 0 0 #313D4A, 5px 0 0 #313D4A',
8: '1px 0 0 #313D4A, -1px 0 0 #313D4A, 0 1px 0 #313D4A, 0 -1px 0 #313D4A, 0 3px 13px rgb(0 0 0 / 8%)'
},
dropShadow: {
1: '0px 1px 0px #E2E8F0',
2: '0px 1px 4px rgba(0, 0, 0, 0.12)'
},
keyframes: {
linspin: {
'100%': { transform: 'rotate(360deg)' }
},
easespin: {
'12.5%': { transform: 'rotate(135deg)' },
'25%': { transform: 'rotate(270deg)' },
'37.5%': { transform: 'rotate(405deg)' },
'50%': { transform: 'rotate(540deg)' },
'62.5%': { transform: 'rotate(675deg)' },
'75%': { transform: 'rotate(810deg)' },
'87.5%': { transform: 'rotate(945deg)' },
'100%': { transform: 'rotate(1080deg)' }
},
'left-spin': {
'0%': { transform: 'rotate(130deg)' },
'50%': { transform: 'rotate(-5deg)' },
'100%': { transform: 'rotate(130deg)' }
},
'right-spin': {
'0%': { transform: 'rotate(-130deg)' },
'50%': { transform: 'rotate(5deg)' },
'100%': { transform: 'rotate(-130deg)' }
},
rotating: {
'0%, 100%': { transform: 'rotate(360deg)' },
'50%': { transform: 'rotate(0deg)' }
},
topbottom: {
'0%, 100%': { transform: 'translate3d(0, -100%, 0)' },
'50%': { transform: 'translate3d(0, 0, 0)' }
},
bottomtop: {
'0%, 100%': { transform: 'translate3d(0, 0, 0)' },
'50%': { transform: 'translate3d(0, -100%, 0)' }
}
},
animation: {
linspin: 'linspin 1568.2353ms linear infinite',
easespin: 'easespin 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both',
'left-spin': 'left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both',
'right-spin': 'right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both',
'ping-once': 'ping 5s cubic-bezier(0, 0, 0.2, 1)',
rotating: 'rotating 30s linear infinite',
topbottom: 'topbottom 60s infinite alternate linear',
bottomtop: 'bottomtop 60s infinite alternate linear',
'spin-1.5': 'spin 1.5s linear infinite',
'spin-2': 'spin 2s linear infinite',
'spin-3': 'spin 3s linear infinite'
}
}
},

View file

@ -2,3 +2,21 @@
# yarn lockfile v1
"@vue/reactivity@~3.1.1":
version "3.1.5"
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.1.5.tgz#dbec4d9557f7c8f25c2635db1e23a78a729eb991"
integrity sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==
dependencies:
"@vue/shared" "3.1.5"
"@vue/shared@3.1.5":
version "3.1.5"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.1.5.tgz#74ee3aad995d0a3996a6bb9533d4d280514ede03"
integrity sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==
alpinejs@^3.13.5:
version "3.13.5"
resolved "https://registry.yarnpkg.com/alpinejs/-/alpinejs-3.13.5.tgz#c06f067e847a295085b4c6f76c42a6a35499d953"
integrity sha512-1d2XeNGN+Zn7j4mUAKXtAgdc4/rLeadyTMWeJGXF5DzwawPBxwTiBhFFm6w/Ei8eJxUZeyNWWSD9zknfdz1kEw==
dependencies:
"@vue/reactivity" "~3.1.1"

View file

@ -27,7 +27,8 @@ defmodule Pinchflat.Profiles.MediaProfile do
schema "media_profiles" do
field :name, :string
field :output_path_template, :string
field :output_path_template, :string, default: "/{{ uploader }}/{{ title }}.{{ ext }}"
field :download_subs, :boolean, default: true
field :download_auto_subs, :boolean, default: true

View file

@ -84,8 +84,10 @@ defmodule PinchflatWeb do
# HTML escaping functionality
import Phoenix.HTML
# Core UI components and translation
import PinchflatWeb.CoreComponents
import PinchflatWeb.Gettext
import PinchflatWeb.CoreComponents
import PinchflatWeb.CustomComponents.TableComponents
import PinchflatWeb.CustomComponents.ButtonComponents
# Shortcut for generating JS commands
alias Phoenix.LiveView.JS

View file

@ -201,46 +201,14 @@ defmodule PinchflatWeb.CoreComponents do
def simple_form(assigns) do
~H"""
<.form :let={f} for={@for} as={@as} {@rest}>
<div class="mt-10 space-y-8 bg-white">
<%= render_slot(@inner_block, f) %>
<div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
<%= render_slot(action, f) %>
</div>
<%= render_slot(@inner_block, f) %>
<div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
<%= render_slot(action, f) %>
</div>
</.form>
"""
end
@doc """
Renders a button.
## Examples
<.button>Send!</.button>
<.button phx-click="go" class="ml-2">Send!</.button>
"""
attr :type, :string, default: nil
attr :class, :string, default: nil
attr :rest, :global, include: ~w(disabled form name value)
slot :inner_block, required: true
def button(assigns) do
~H"""
<button
type={@type}
class={[
"phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3",
"text-sm font-semibold leading-6 text-white active:text-white/80",
@class
]}
{@rest}
>
<%= render_slot(@inner_block) %>
</button>
"""
end
@doc """
Renders an input with label and error messages.
@ -270,11 +238,12 @@ defmodule PinchflatWeb.CoreComponents do
attr :name, :any
attr :label, :string, default: nil
attr :value, :any
attr :help, :string, default: nil
attr :type, :string,
default: "text",
values: ~w(checkbox color date datetime-local email file hidden month number password
range radio search select tel text textarea time url week)
toggle range radio search select tel text textarea time url week)
attr :field, Phoenix.HTML.FormField,
doc: "a form field struct retrieved from the form, for example: @form[:email]"
@ -308,7 +277,7 @@ defmodule PinchflatWeb.CoreComponents do
~H"""
<div phx-feedback-for={@name}>
<label class="flex items-center gap-4 text-sm leading-6 text-zinc-600">
<label class="flex items-center gap-4 text-sm leading-6">
<input type="hidden" name={@name} value="false" />
<input
type="checkbox"
@ -316,16 +285,54 @@ defmodule PinchflatWeb.CoreComponents do
name={@name}
value="true"
checked={@checked}
class="rounded border-zinc-300 text-zinc-900 focus:ring-0"
class="rounded focus:ring-0"
{@rest}
/>
<%= @label %>
</label>
<.help :if={@help}><%= @help %></.help>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
end
def input(%{type: "toggle"} = assigns) do
assigns =
assign_new(assigns, :checked, fn ->
Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value])
end)
~H"""
<div x-data={"{ enabled: #{@checked}}"}>
<.label for={@id}><%= @label %></.label>
<div class="relative">
<input type="hidden" name={@name} value="false" />
<input
type="checkbox"
id={@id}
name={@name}
value="true"
x-bind:checked="enabled"
class="sr-only"
@change="enabled = !enabled"
{@rest}
/>
<div class="inline-block cursor-pointer" @click="enabled = !enabled">
<div x-bind:class="enabled && '!bg-primary'" class="block h-8 w-14 rounded-full bg-black">
</div>
<div
x-bind:class="enabled && '!right-1 !translate-x-full'"
class="absolute left-1 top-1 flex h-6 w-6 items-center justify-center rounded-full bg-white transition"
>
</div>
</div>
<.help :if={@help}><%= @help %></.help>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
</div>
"""
end
def input(%{type: "select"} = assigns) do
~H"""
<div phx-feedback-for={@name}>
@ -333,13 +340,17 @@ defmodule PinchflatWeb.CoreComponents do
<select
id={@id}
name={@name}
class="mt-2 block w-full rounded-md border border-gray-300 bg-white shadow-sm focus:border-zinc-400 focus:ring-0 sm:text-sm"
class={[
"relative z-20 w-full appearance-none rounded border border-stroke bg-transparent py-3 pl-5 pr-12 outline-none transition",
"focus:border-primary active:border-primary dark:border-form-strokedark dark:bg-form-input text-black dark:text-white"
]}
multiple={@multiple}
{@rest}
>
<option :if={@prompt} value=""><%= @prompt %></option>
<%= Phoenix.HTML.Form.options_for_select(@options, @value) %>
</select>
<.help :if={@help}><%= @help %></.help>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
@ -360,6 +371,7 @@ defmodule PinchflatWeb.CoreComponents do
]}
{@rest}
><%= Phoenix.HTML.Form.normalize_value("textarea", @value) %></textarea>
<.help :if={@help}><%= @help %></.help>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
@ -376,18 +388,32 @@ defmodule PinchflatWeb.CoreComponents do
id={@id}
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
class={[
"mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6",
"phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400",
@errors == [] && "border-zinc-300 focus:border-zinc-400",
"w-full rounded-lg border-[1.5px] border-stroke bg-transparent px-5 py-3 font-normal text-black",
"outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter",
"dark:border-form-strokedark dark:bg-form-input dark:text-white dark:focus:border-primary",
@errors != [] && "border-rose-400 focus:border-rose-400"
]}
{@rest}
/>
<.help :if={@help}><%= @help %></.help>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
end
@doc """
Renders help text.
"""
slot :inner_block, required: true
def help(assigns) do
~H"""
<p class="mt-1 text-sm leading-5">
<%= render_slot(@inner_block) %>
</p>
"""
end
@doc """
Renders a label.
"""
@ -396,7 +422,7 @@ defmodule PinchflatWeb.CoreComponents do
def label(assigns) do
~H"""
<label for={@for} class="block text-sm font-semibold leading-6 text-zinc-800">
<label for={@for} class="mt-5 mb-2 block text-md font-medium text-black dark:text-white">
<%= render_slot(@inner_block) %>
</label>
"""
@ -409,7 +435,7 @@ defmodule PinchflatWeb.CoreComponents do
def error(assigns) do
~H"""
<p class="mt-3 flex gap-3 text-sm leading-6 text-rose-600 phx-no-feedback:hidden">
<p class="mt-1 mb-5 flex gap-3 text-md leading-6 text-rose-600 phx-no-feedback:hidden">
<.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" />
<%= render_slot(@inner_block) %>
</p>
@ -425,7 +451,7 @@ defmodule PinchflatWeb.CoreComponents do
slot :subtitle
slot :actions
def header(assigns) do
def old_header(assigns) do
~H"""
<header class={[@actions != [] && "flex items-center justify-between gap-6", @class]}>
<div>
@ -446,10 +472,10 @@ defmodule PinchflatWeb.CoreComponents do
## Examples
<.table id="users" rows={@users}>
<.old_table id="users" rows={@users}>
<:col :let={user} label="id"><%= user.id %></:col>
<:col :let={user} label="username"><%= user.username %></:col>
</.table>
</.old_table>
"""
attr :id, :string, required: true
attr :rows, :list, required: true
@ -466,56 +492,54 @@ defmodule PinchflatWeb.CoreComponents do
slot :action, doc: "the slot for showing user actions in the last table column"
def table(assigns) do
def old_table(assigns) do
assigns =
with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end)
end
~H"""
<div class="overflow-y-auto px-4 sm:overflow-visible sm:px-0">
<table class="w-[40rem] mt-11 sm:w-full">
<thead class="text-sm text-left leading-6 text-zinc-500">
<tr>
<th :for={col <- @col} class="p-0 pb-4 pr-6 font-normal"><%= col[:label] %></th>
<th :if={@action != []} class="relative p-0 pb-4">
<span class="sr-only"><%= gettext("Actions") %></span>
</th>
</tr>
</thead>
<tbody
id={@id}
phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}
class="relative divide-y divide-zinc-100 border-t border-zinc-200 text-sm leading-6 text-zinc-700"
>
<tr :for={row <- @rows} id={@row_id && @row_id.(row)} class="group hover:bg-zinc-50">
<td
:for={{col, i} <- Enum.with_index(@col)}
phx-click={@row_click && @row_click.(row)}
class={["relative p-0", @row_click && "hover:cursor-pointer"]}
>
<div class="block py-4 pr-6">
<span class="absolute -inset-y-px right-0 -left-4 group-hover:bg-zinc-50 sm:rounded-l-xl" />
<span class={["relative", i == 0 && "font-semibold text-zinc-900"]}>
<%= render_slot(col, @row_item.(row)) %>
</span>
</div>
</td>
<td :if={@action != []} class="relative w-14 p-0">
<div class="relative whitespace-nowrap py-4 text-right text-sm font-medium">
<span class="absolute -inset-y-px -right-4 left-0 group-hover:bg-zinc-50 sm:rounded-r-xl" />
<span
:for={action <- @action}
class="relative ml-4 font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
>
<%= render_slot(action, @row_item.(row)) %>
</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<table class="w-[40rem] mt-11 sm:w-full">
<thead class="text-sm text-left leading-6 text-zinc-500">
<tr>
<th :for={col <- @col} class="p-0 pb-4 pr-6 font-normal"><%= col[:label] %></th>
<th :if={@action != []} class="relative p-0 pb-4">
<span class="sr-only"><%= gettext("Actions") %></span>
</th>
</tr>
</thead>
<tbody
id={@id}
phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}
class="relative divide-y divide-zinc-100 border-t border-zinc-200 text-sm leading-6 text-zinc-700"
>
<tr :for={row <- @rows} id={@row_id && @row_id.(row)} class="group hover:bg-zinc-50">
<td
:for={{col, i} <- Enum.with_index(@col)}
phx-click={@row_click && @row_click.(row)}
class={["relative p-0", @row_click && "hover:cursor-pointer"]}
>
<div class="block py-4 pr-6">
<span class="absolute -inset-y-px right-0 -left-4 group-hover:bg-zinc-50 sm:rounded-l-xl" />
<span class={["relative", i == 0 && "font-semibold text-zinc-900"]}>
<%= render_slot(col, @row_item.(row)) %>
</span>
</div>
</td>
<td :if={@action != []} class="relative w-14 p-0">
<div class="relative whitespace-nowrap py-4 text-right text-sm font-medium">
<span class="absolute -inset-y-px -right-4 left-0 group-hover:bg-zinc-50 sm:rounded-r-xl" />
<span
:for={action <- @action}
class="relative ml-4 font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
>
<%= render_slot(action, @row_item.(row)) %>
</span>
</div>
</td>
</tr>
</tbody>
</table>
"""
end
@ -536,10 +560,10 @@ defmodule PinchflatWeb.CoreComponents do
def list(assigns) do
~H"""
<div class="mt-2 mb-14">
<dl class="-my-4 divide-y divide-zinc-100">
<dl class="-my-4 divide-y dark:divide-strokedark">
<div :for={item <- @item} class="flex gap-4 py-4 text-sm leading-6 sm:gap-8">
<dt class="w-1/4 flex-none text-zinc-500"><%= item.title %></dt>
<dd class="text-zinc-700"><%= render_slot(item) %></dd>
<dt class="w-1/4 flex-none dark:text-white"><%= item.title %></dt>
<dd class="dark:text-white"><%= render_slot(item) %></dd>
</div>
</dl>
</div>

View file

@ -0,0 +1,33 @@
defmodule PinchflatWeb.CustomComponents.ButtonComponents do
@moduledoc false
use Phoenix.Component
@doc """
Render a button
## Examples
<.button color="bg-primary" rounding="rounded-sm">
<span>Click me</span>
</.button>
"""
attr :color, :string, default: "bg-primary"
attr :rounding, :string, default: "rounded-sm"
attr :class, :string, default: ""
slot :inner_block, required: true
def button(assigns) do
~H"""
<button class={[
"text-center font-medium text-white",
"#{@rounding} inline-flex items-center justify-center px-10 py-4",
"#{@color}",
"hover:bg-opacity-90 lg:px-8 xl:px-10",
@class
]}>
<%= render_slot(@inner_block) %>
</button>
"""
end
end

View file

@ -0,0 +1,53 @@
defmodule PinchflatWeb.CustomComponents.TableComponents do
@moduledoc false
use Phoenix.Component
@doc """
Renders a table component with the given rows and columns.
## Examples
<.table rows={@users}>
<:col :let={user} label="Name"><%= user.name %></:col>
</.table>
"""
attr :rows, :list, required: true
attr :table_class, :string, default: ""
attr :row_item, :any,
default: &Function.identity/1,
doc: "the function for mapping each row before calling the :col and :action slots"
slot :col, required: true do
attr :label, :string
attr :class, :string
end
def table(assigns) do
~H"""
<table class={["w-full table-auto", @table_class]}>
<thead>
<tr class="bg-gray-2 text-left dark:bg-meta-4">
<th :for={col <- @col} class="px-4 py-4 font-medium text-black dark:text-white xl:pl-11">
<%= col[:label] %>
</th>
</tr>
</thead>
<tbody>
<tr :for={{row, i} <- Enum.with_index(@rows)}>
<td
:for={col <- @col}
class={[
"px-4 py-5 pl-9 dark:border-strokedark xl:pl-11",
i + 1 > length(@rows) && "border-b border-[#eee] dark:border-π",
col[:class]
]}
>
<%= render_slot(col, @row_item.(row)) %>
</td>
</tr>
</tbody>
</table>
"""
end
end

View file

@ -2,4 +2,28 @@ defmodule PinchflatWeb.Layouts do
use PinchflatWeb, :html
embed_templates "layouts/*"
embed_templates "layouts/partials/*"
attr :icon, :string, required: true
attr :text, :string, required: true
attr :navigate, :any, required: true
def sidebar_item(assigns) do
# I'm testing out grouping classes here. Tentative order: font, layout, color, animation, state-modifiers
~H"""
<li>
<.link
navigate={@navigate}
class={[
"font-medium text-bodydark1",
"group relative flex items-center gap-2.5 rounded-sm px-4 py-2 duration-300 ease-in-out",
"duration-300 ease-in-out",
"hover:bg-graydark dark:hover:bg-meta-4"
]}
>
<.icon name={@icon} /> <%= @text %>
</.link>
</li>
"""
end
end

View file

@ -1,32 +1,13 @@
<header class="px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between border-b border-zinc-100 py-3 text-sm">
<div class="flex items-center gap-4">
<a href="/">
<img src={~p"/images/logo.svg"} width="36" />
</a>
<p class="bg-brand/5 text-brand rounded-full px-2 font-medium leading-6">
v<%= Application.spec(:phoenix, :vsn) %>
</p>
</div>
<div class="flex items-center gap-4 font-semibold leading-6 text-zinc-900">
<a href="https://twitter.com/elixirphoenix" class="hover:text-zinc-700">
@elixirphoenix
</a>
<a href="https://github.com/phoenixframework/phoenix" class="hover:text-zinc-700">
GitHub
</a>
<a
href="https://hexdocs.pm/phoenix/overview.html"
class="rounded-lg bg-zinc-100 px-2 py-1 hover:bg-zinc-200/80"
>
Get Started <span aria-hidden="true">&rarr;</span>
</a>
</div>
<div class="flex h-screen overflow-hidden">
<.sidebar />
<div class="relative flex flex-1 flex-col overflow-y-auto overflow-x-hidden">
<.header />
<main>
<div class="mx-auto max-w-screen-2xl p-4 md:p-6 2xl:p-10">
<.flash_group flash={@flash} />
<%= @inner_content %>
</div>
</main>
</div>
</header>
<main class="px-4 py-20 sm:px-6 lg:px-8">
<div class="mx-auto max-w-2xl">
<.flash_group flash={@flash} />
<%= @inner_content %>
</div>
</main>
</div>

View file

@ -0,0 +1,29 @@
<header class="sticky top-0 z-999 flex w-full bg-white drop-shadow-1 dark:bg-boxdark dark:drop-shadow-none">
<div class="flex flex-grow items-center justify-end px-4 py-4 shadow-2 md:px-6 2xl:px-11">
<div class="flex items-center gap-2 sm:gap-4 lg:hidden">
<button
class="z-99999 block rounded-sm border border-stroke bg-white p-1.5 shadow-sm dark:border-strokedark dark:bg-boxdark lg:hidden"
@click.stop="sidebarToggle = !sidebarToggle"
>
<.icon name="hero-bars-3" />
</button>
<a class="block flex-shrink-0 lg:hidden" href="#">
<h2 class="text-title-md2 font-bold text-white">Pinchflat</h2>
</a>
</div>
<div class="hidden sm:block bg-meta-4 rounded-md">
<%!-- Aspirational (for now) --%>
<div class="relative">
<span class="absolute left-2 top-1/2 -translate-y-1/2 flex">
<.icon name="hero-magnifying-glass" />
</span>
<input
type="text"
placeholder="Type to search..."
class="w-full bg-transparent pl-9 pr-4 border-0 focus:ring-0 focus:outline-none xl:w-125"
/>
</div>
</div>
</div>
</header>

View file

@ -0,0 +1,29 @@
<aside
x-bind:class="sidebarToggle ? 'translate-x-0' : '-translate-x-full'"
class="absolute left-0 top-0 z-9999 flex h-screen w-72.5 flex-col overflow-y-hidden bg-black duration-300 ease-linear dark:bg-boxdark lg:static lg:translate-x-0"
@click.outside="sidebarToggle = false"
>
<div class="flex items-center justify-between gap-2 px-6 py-5.5 lg:py-6.5">
<h2 class="text-title-md2 font-bold text-white">Pinchflat</h2>
<button class="block lg:hidden" @click.stop="sidebarToggle = !sidebarToggle">
<.icon name="hero-arrow-left" class="fill-current" />
</button>
</div>
<div class="no-scrollbar flex flex-col overflow-y-auto duration-300 ease-linear">
<nav class="mt-5 px-4 py-4 lg:mt-9 lg:px-6">
<div>
<h3 class="mb-4 ml-4 text-sm font-medium text-bodydark2">MENU</h3>
<ul class="mb-6 flex flex-col gap-1.5">
<.sidebar_item icon="hero-tv" text="Sources" navigate={~p"/sources"} />
<.sidebar_item
icon="hero-adjustments-vertical"
text="Media Profiles"
navigate={~p"/media_profiles"}
/>
</ul>
</div>
</nav>
</div>
</aside>

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en" class="[scrollbar-gutter:stable]">
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
@ -11,7 +11,7 @@
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
</script>
</head>
<body class="bg-white antialiased">
<body x-data="{ 'sidebarToggle': false }" class="dark text-bodydark bg-boxdark-2">
<%= @inner_content %>
</body>
</html>

View file

@ -1,8 +1,14 @@
<.header>
Edit Media profile <%= @media_profile.id %>
<:subtitle>Use this form to manage media_profile records in your database.</:subtitle>
</.header>
<div class="mb-6 flex gap-3 flex-row items-center">
<.link navigate={~p"/media_profiles"}>
<.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
</.link>
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4">Edit Media Profile</h2>
</div>
<.media_profile_form changeset={@changeset} action={~p"/media_profiles/#{@media_profile}"} />
<.back navigate={~p"/media_profiles"}>Back to media_profiles</.back>
<div class="rounded-sm border border-stroke bg-white px-5 pb-2.5 pt-6 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
<div class="max-w-full overflow-x-auto">
<div class="flex flex-col gap-10">
<.media_profile_form changeset={@changeset} action={~p"/media_profiles/#{@media_profile}"} />
</div>
</div>
</div>

View file

@ -1,30 +1,39 @@
<.header>
Listing Media profiles
<:actions>
<.link href={~p"/media_profiles/new"}>
<.button>New Media profile</.button>
<div class="mb-6 flex gap-3 flex-row items-center justify-between">
<h2 class="text-title-md2 font-bold text-black dark:text-white">All Media Profiles</h2>
<nav>
<.link navigate={~p"/media_profiles/new"}>
<.button color="bg-primary" rounding="rounded-full">
<span class="font-bold mx-2">+</span> New Media Profile
</.button>
</.link>
</:actions>
</.header>
</nav>
</div>
<.table
id="media_profiles"
rows={@media_profiles}
row_click={&JS.navigate(~p"/media_profiles/#{&1}")}
>
<:col :let={media_profile} label="Name"><%= media_profile.name %></:col>
<:col :let={media_profile} label="Output path template">
<%= media_profile.output_path_template %>
</:col>
<:action :let={media_profile}>
<div class="sr-only">
<.link navigate={~p"/media_profiles/#{media_profile}"}>Show</.link>
<div class="rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark">
<div class="max-w-full overflow-x-auto">
<div class="flex flex-col gap-10">
<.table rows={@media_profiles} table_class="text-black dark:text-white">
<:col :let={media_profile} label="Name">
<%= media_profile.name %>
</:col>
<:col :let={media_profile} label="Output Template">
<code class="text-sm"><%= media_profile.output_path_template %></code>
</:col>
<:col :let={media_profile} label="" class="flex place-content-evenly">
<.link
navigate={~p"/media_profiles/#{media_profile.id}"}
class="hover:text-secondary duration-200 ease-in-out mx-0.5"
>
<.icon name="hero-eye" />
</.link>
<.link
navigate={~p"/media_profiles/#{media_profile.id}/edit"}
class="hover:text-secondary duration-200 ease-in-out mx-0.5"
>
<.icon name="hero-pencil-square" />
</.link>
</:col>
</.table>
</div>
<.link navigate={~p"/media_profiles/#{media_profile}/edit"}>Edit</.link>
</:action>
<:action :let={media_profile}>
<.link href={~p"/media_profiles/#{media_profile}"} method="delete" data-confirm="Are you sure?">
Delete
</.link>
</:action>
</.table>
</div>
</div>

View file

@ -2,29 +2,62 @@
<.error :if={@changeset.action}>
Oops, something went wrong! Please check the errors below.
</.error>
<.input field={f[:name]} type="text" label="Name" />
<.input field={f[:output_path_template]} type="text" label="Output path template" />
<h3 class="my-4 text-2xl text-black dark:text-white">
General Options
</h3>
<.input field={f[:name]} type="text" label="Name" placeholder="New Profile" help="(required)" />
<.input
field={f[:output_path_template]}
type="text"
label="Output path template"
help="TODO: provide docs (required)"
/>
<h3>Subtitle Options</h3>
<.input field={f[:download_subs]} type="checkbox" label="Download Subs" />
<.input field={f[:download_auto_subs]} type="checkbox" label="Download Autogenerated Subs" />
<.input field={f[:embed_subs]} type="checkbox" label="Embed Subs" />
<.input field={f[:sub_langs]} type="text" label="Sub Langs" />
<h3 class="mb-4 mt-8 text-2xl text-black dark:text-white">
Subtitle Options
</h3>
<.input field={f[:download_subs]} type="toggle" label="Download Subtitles" />
<.input
field={f[:download_auto_subs]}
type="toggle"
label="Download Autogenerated Subtitles"
help="Prefers normal subs but will download autogenerated if needed"
/>
<.input
field={f[:embed_subs]}
type="toggle"
label="Embed Subtitles"
help="Embeds subtitles in the video file itself, if supported"
/>
<.input
field={f[:sub_langs]}
type="text"
label="Subtitle Languages"
help="Use commas for multiple languages (eg: en,de)"
/>
<h3>Thumbnail Options</h3>
<.input field={f[:download_thumbnail]} type="checkbox" label="Download Thumbnail" />
<.input field={f[:embed_thumbnail]} type="checkbox" label="Embed Thumbnail" />
<h3 class="mb-4 mt-8 text-2xl text-black dark:text-white">
Thumbnail Options
</h3>
<.input field={f[:download_thumbnail]} type="toggle" label="Download Thumbnail" />
<.input field={f[:embed_thumbnail]} type="toggle" label="Embed Thumbnail" />
<h3>Metadata Options</h3>
<.input field={f[:download_metadata]} type="checkbox" label="Download Metadata" />
<.input field={f[:embed_metadata]} type="checkbox" label="Embed Metadata" />
<h3 class="mb-4 mt-8 text-2xl text-black dark:text-white">
Metadata Options
</h3>
<.input field={f[:download_metadata]} type="toggle" label="Download Metadata" />
<.input field={f[:embed_metadata]} type="toggle" label="Embed Metadata" />
<h3 class="mb-4 mt-8 text-2xl text-black dark:text-white">
Release Format Options
</h3>
<h3>Release Format Options</h3>
<.input
field={f[:shorts_behaviour]}
options={friendly_format_type_options()}
type="select"
label="Include Shorts?"
help="Experimental"
/>
<.input
field={f[:livestream_behaviour]}
@ -34,6 +67,6 @@
/>
<:actions>
<.button>Save Media profile</.button>
<.button class="mt-15 mb-5 sm:mb-7.5">Save Media profile</.button>
</:actions>
</.simple_form>

View file

@ -1,8 +1,14 @@
<.header>
New Media profile
<:subtitle>Use this form to manage media_profile records in your database.</:subtitle>
</.header>
<div class="mb-6 flex gap-3 flex-row items-center">
<.link navigate={~p"/media_profiles"}>
<.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
</.link>
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4">New Media Profile</h2>
</div>
<.media_profile_form changeset={@changeset} action={~p"/media_profiles"} />
<.back navigate={~p"/media_profiles"}>Back to media_profiles</.back>
<div class="rounded-sm border border-stroke bg-white px-5 pb-2.5 pt-6 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
<div class="max-w-full overflow-x-auto">
<div class="flex flex-col gap-10">
<.media_profile_form changeset={@changeset} action={~p"/media_profiles"} />
</div>
</div>
</div>

View file

@ -1,13 +1,26 @@
<.header>
Media profile <%= @media_profile.id %>
<:subtitle>This is a media_profile record from your database.</:subtitle>
<:actions>
<.link href={~p"/media_profiles/#{@media_profile}/edit"}>
<.button>Edit media_profile</.button>
<div class="mb-6 flex gap-3 flex-row items-center justify-between">
<div class="flex gap-3 items-center">
<.link navigate={~p"/media_profiles"}>
<.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
</.link>
</:actions>
</.header>
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4">
Media Profile #<%= @media_profile.id %>
</h2>
</div>
<.list_items_from_map map={Map.from_struct(@media_profile)} />
<.back navigate={~p"/media_profiles"}>Back to media_profiles</.back>
<nav>
<.link navigate={~p"/media_profiles/#{@media_profile}/edit"}>
<.button color="bg-primary" rounding="rounded-full">
<.icon name="hero-pencil-square" class="mr-2" /> Edit Media Profile
</.button>
</.link>
</nav>
</div>
<div class="rounded-sm border border-stroke bg-white px-5 pb-2.5 pt-6 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
<div class="max-w-full overflow-x-auto">
<div class="flex flex-col gap-10 dark:text-white">
<h3 class="font-bold text-xl">Attributes</h3>
<.list_items_from_map map={Map.from_struct(@media_profile)} />
</div>
</div>
</div>

View file

@ -7,7 +7,7 @@ defmodule PinchflatWeb.MediaSources.SourceController do
alias Pinchflat.MediaSource.Source
def index(conn, _params) do
sources = MediaSource.list_sources()
sources = Repo.preload(MediaSource.list_sources(), :media_profile)
render(conn, :index, sources: sources)
end
@ -23,7 +23,7 @@ defmodule PinchflatWeb.MediaSources.SourceController do
{:ok, source} ->
conn
|> put_flash(:info, "Source created successfully.")
|> redirect(to: ~p"/media_sources/sources/#{source}")
|> redirect(to: ~p"/sources/#{source}")
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, :new, changeset: changeset, media_profiles: media_profiles())
@ -53,7 +53,7 @@ defmodule PinchflatWeb.MediaSources.SourceController do
{:ok, source} ->
conn
|> put_flash(:info, "Source updated successfully.")
|> redirect(to: ~p"/media_sources/sources/#{source}")
|> redirect(to: ~p"/sources/#{source}")
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, :edit,
@ -70,7 +70,7 @@ defmodule PinchflatWeb.MediaSources.SourceController do
conn
|> put_flash(:info, "Source deleted successfully.")
|> redirect(to: ~p"/media_sources/sources")
|> redirect(to: ~p"/sources")
end
defp media_profiles do

View file

@ -1,12 +1,18 @@
<.header>
Edit Source <%= @source.id %>
<:subtitle>Use this form to manage source records in your database.</:subtitle>
</.header>
<div class="mb-6 flex gap-3 flex-row items-center">
<.link navigate={~p"/sources"}>
<.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
</.link>
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4">Edit Source</h2>
</div>
<.source_form
changeset={@changeset}
media_profiles={@media_profiles}
action={~p"/media_sources/sources/#{@source}"}
/>
<.back navigate={~p"/media_sources/sources"}>Back to sources</.back>
<div class="rounded-sm border border-stroke bg-white px-5 pb-2.5 pt-6 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
<div class="max-w-full overflow-x-auto">
<div class="flex flex-col gap-10">
<.source_form
changeset={@changeset}
media_profiles={@media_profiles}
action={~p"/sources/#{@source}"}
/>
</div>
</div>
</div>

View file

@ -1,24 +1,48 @@
<.header>
Listing Sources
<:actions>
<.link href={~p"/media_sources/sources/new"}>
<.button>New Source</.button>
<div class="mb-6 flex gap-3 flex-row items-center justify-between">
<h2 class="text-title-md2 font-bold text-black dark:text-white">All Sources</h2>
<nav>
<.link navigate={~p"/sources/new"}>
<.button color="bg-primary" rounding="rounded-full">
<span class="font-bold mx-2">+</span> New Source
</.button>
</.link>
</:actions>
</.header>
</nav>
</div>
<.table id="sources" rows={@sources} row_click={&JS.navigate(~p"/media_sources/sources/#{&1}")}>
<:col :let={source} label="Collection Name"><%= source.collection_name %></:col>
<:col :let={source} label="Collection ID"><%= source.collection_id %></:col>
<:action :let={source}>
<div class="sr-only">
<.link navigate={~p"/media_sources/sources/#{source}"}>Show</.link>
<div class="rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark">
<div class="max-w-full overflow-x-auto">
<div class="flex flex-col gap-10">
<.table rows={@sources} table_class="text-black dark:text-white">
<:col :let={source} label="Name">
<%= source.friendly_name || source.collection_name %>
</:col>
<:col :let={source} label="Type"><%= source.collection_type %></:col>
<:col :let={source} label="Should Download?">
<.icon name={if source.download_media, do: "hero-check", else: "hero-x-mark"} />
</:col>
<:col :let={source} label="Media Profile">
<.link
navigate={~p"/media_profiles/#{source.media_profile_id}"}
class="hover:text-secondary duration-200 ease-in-out"
>
<%= source.media_profile.name %>
</.link>
</:col>
<:col :let={source} label="" class="flex place-content-evenly">
<.link
navigate={~p"/sources/#{source.id}"}
class="hover:text-secondary duration-200 ease-in-out mx-0.5"
>
<.icon name="hero-eye" />
</.link>
<.link
navigate={~p"/sources/#{source.id}/edit"}
class="hover:text-secondary duration-200 ease-in-out mx-0.5"
>
<.icon name="hero-pencil-square" />
</.link>
</:col>
</.table>
</div>
<.link navigate={~p"/media_sources/sources/#{source}/edit"}>Edit</.link>
</:action>
<:action :let={source}>
<.link href={~p"/media_sources/sources/#{source}"} method="delete" data-confirm="Are you sure?">
Delete
</.link>
</:action>
</.table>
</div>
</div>

View file

@ -1,12 +1,14 @@
<.header>
New Source
<:subtitle>Use this form to manage source records in your database.</:subtitle>
</.header>
<div class="mb-6 flex gap-3 flex-row items-center">
<.link navigate={~p"/sources"}>
<.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
</.link>
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4">New Source</h2>
</div>
<.source_form
changeset={@changeset}
media_profiles={@media_profiles}
action={~p"/media_sources/sources"}
/>
<.back navigate={~p"/media_sources/sources"}>Back to sources</.back>
<div class="rounded-sm border border-stroke bg-white px-5 pb-2.5 pt-6 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
<div class="max-w-full overflow-x-auto">
<div class="flex flex-col gap-10">
<.source_form changeset={@changeset} media_profiles={@media_profiles} action={~p"/sources"} />
</div>
</div>
</div>

View file

@ -1,23 +1,38 @@
<.header>
Source <%= @source.id %>
<:subtitle>This is a source record from your database.</:subtitle>
<:actions>
<.link href={~p"/media_sources/sources/#{@source}/edit"}>
<.button>Edit source</.button>
<div class="mb-6 flex gap-3 flex-row items-center justify-between">
<div class="flex gap-3 items-center">
<.link navigate={~p"/sources"}>
<.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
</.link>
</:actions>
</.header>
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4">
Source #<%= @source.id %>
</h2>
</div>
<h3 class="mt-14">Relationships</h3>
<.list>
<:item title="media_profile">
<.link href={~p"/media_profiles/#{@source.media_profile}"}>
<%= @source.media_profile.name %>
<nav>
<.link navigate={~p"/sources/#{@source}/edit"}>
<.button color="bg-primary" rounding="rounded-full">
<.icon name="hero-pencil-square" class="mr-2" /> Edit Source
</.button>
</.link>
</:item>
</.list>
</nav>
</div>
<div class="rounded-sm border border-stroke bg-white px-5 pb-2.5 pt-6 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
<div class="max-w-full overflow-x-auto">
<div class="flex flex-col gap-10 dark:text-white">
<h3 class="mt-14 font-bold text-xl">Relationships</h3>
<.list>
<:item title="media_profile">
<.link
navigate={~p"/media_profiles/#{@source.media_profile_id}"}
class="hover:text-secondary duration-200 ease-in-out"
>
<%= @source.media_profile.name %>
</.link>
</:item>
</.list>
<h3>Attributes</h3>
<.list_items_from_map map={Map.from_struct(@source)} />
<.back navigate={~p"/media_sources/sources"}>Back to sources</.back>
<h3 class="font-bold text-xl">Attributes</h3>
<.list_items_from_map map={Map.from_struct(@source)} />
</div>
</div>
</div>

View file

@ -3,6 +3,13 @@
Oops, something went wrong! Please check the errors below.
</.error>
<.input
field={f[:original_url]}
type="text"
label="Source URL"
help="URL of a channel or playlist (required)"
/>
<.input field={f[:friendly_name]} type="text" label="Friendly Name" />
<.input
@ -16,21 +23,25 @@
field={f[:collection_type]}
options={friendly_collection_types()}
type="select"
label="Collection Type"
label="Source Type"
/>
<.input field={f[:original_url]} type="text" label="Source URL" />
<.input
field={f[:index_frequency_minutes]}
options={friendly_index_frequencies()}
type="select"
label="Index Frequency"
help="Roughly how often to check for media to download"
/>
<.input field={f[:download_media]} type="checkbox" label="Download Media?" />
<.input
field={f[:download_media]}
type="toggle"
label="Download Media?"
help="Unchecking still indexes media but it won't be downloaded"
/>
<:actions>
<.button>Save Source</.button>
<.button class="mt-15 mb-5 sm:mb-7.5">Save Source</.button>
</:actions>
</.simple_form>

View file

@ -4,6 +4,6 @@ defmodule PinchflatWeb.PageController do
def home(conn, _params) do
# The home page is often custom made,
# so skip the default app layout.
render(conn, :home, layout: false)
render(conn, :home)
end
end

View file

@ -20,10 +20,7 @@ defmodule PinchflatWeb.Router do
get "/", PageController, :home
resources "/media_profiles", MediaProfiles.MediaProfileController
scope "/media_sources", MediaSources do
resources "/sources", SourceController
end
resources "/sources", MediaSources.SourceController
end
# Other scopes may use custom stacks.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -13,14 +13,14 @@ defmodule PinchflatWeb.MediaProfileControllerTest do
describe "index" do
test "lists all media_profiles", %{conn: conn} do
conn = get(conn, ~p"/media_profiles")
assert html_response(conn, 200) =~ "Listing Media profiles"
assert html_response(conn, 200) =~ "All Media Profiles"
end
end
describe "new media_profile" do
test "renders form", %{conn: conn} do
conn = get(conn, ~p"/media_profiles/new")
assert html_response(conn, 200) =~ "New Media profile"
assert html_response(conn, 200) =~ "New Media Profile"
end
end
@ -32,12 +32,12 @@ defmodule PinchflatWeb.MediaProfileControllerTest do
assert redirected_to(conn) == ~p"/media_profiles/#{id}"
conn = get(conn, ~p"/media_profiles/#{id}")
assert html_response(conn, 200) =~ "Media profile #{id}"
assert html_response(conn, 200) =~ "Media Profile ##{id}"
end
test "renders errors when data is invalid", %{conn: conn} do
conn = post(conn, ~p"/media_profiles", media_profile: @invalid_attrs)
assert html_response(conn, 200) =~ "New Media profile"
assert html_response(conn, 200) =~ "New Media Profile"
end
end
@ -49,7 +49,7 @@ defmodule PinchflatWeb.MediaProfileControllerTest do
media_profile: media_profile
} do
conn = get(conn, ~p"/media_profiles/#{media_profile}/edit")
assert html_response(conn, 200) =~ "Edit Media profile"
assert html_response(conn, 200) =~ "Edit Media Profile"
end
end
@ -66,7 +66,7 @@ defmodule PinchflatWeb.MediaProfileControllerTest do
test "renders errors when data is invalid", %{conn: conn, media_profile: media_profile} do
conn = put(conn, ~p"/media_profiles/#{media_profile}", media_profile: @invalid_attrs)
assert html_response(conn, 200) =~ "Edit Media profile"
assert html_response(conn, 200) =~ "Edit Media Profile"
end
end

View file

@ -28,14 +28,14 @@ defmodule PinchflatWeb.SourceControllerTest do
describe "index" do
test "lists all sources", %{conn: conn} do
conn = get(conn, ~p"/media_sources/sources")
assert html_response(conn, 200) =~ "Listing Sources"
conn = get(conn, ~p"/sources")
assert html_response(conn, 200) =~ "All Sources"
end
end
describe "new source" do
test "renders form", %{conn: conn} do
conn = get(conn, ~p"/media_sources/sources/new")
conn = get(conn, ~p"/sources/new")
assert html_response(conn, 200) =~ "New Source"
end
end
@ -43,17 +43,17 @@ defmodule PinchflatWeb.SourceControllerTest do
describe "create source" do
test "redirects to show when data is valid", %{conn: conn, create_attrs: create_attrs} do
expect(YtDlpRunnerMock, :run, 1, &runner_function_mock/3)
conn = post(conn, ~p"/media_sources/sources", source: create_attrs)
conn = post(conn, ~p"/sources", source: create_attrs)
assert %{id: id} = redirected_params(conn)
assert redirected_to(conn) == ~p"/media_sources/sources/#{id}"
assert redirected_to(conn) == ~p"/sources/#{id}"
conn = get(conn, ~p"/media_sources/sources/#{id}")
assert html_response(conn, 200) =~ "Source #{id}"
conn = get(conn, ~p"/sources/#{id}")
assert html_response(conn, 200) =~ "Source ##{id}"
end
test "renders errors when data is invalid", %{conn: conn, invalid_attrs: invalid_attrs} do
conn = post(conn, ~p"/media_sources/sources", source: invalid_attrs)
conn = post(conn, ~p"/sources", source: invalid_attrs)
assert html_response(conn, 200) =~ "New Source"
end
end
@ -62,7 +62,7 @@ defmodule PinchflatWeb.SourceControllerTest do
setup [:create_source]
test "renders form for editing chosen source", %{conn: conn, source: source} do
conn = get(conn, ~p"/media_sources/sources/#{source}/edit")
conn = get(conn, ~p"/sources/#{source}/edit")
assert html_response(conn, 200) =~ "Edit Source"
end
end
@ -73,10 +73,10 @@ defmodule PinchflatWeb.SourceControllerTest do
test "redirects when data is valid", %{conn: conn, source: source, update_attrs: update_attrs} do
expect(YtDlpRunnerMock, :run, 1, &runner_function_mock/3)
conn = put(conn, ~p"/media_sources/sources/#{source}", source: update_attrs)
assert redirected_to(conn) == ~p"/media_sources/sources/#{source}"
conn = put(conn, ~p"/sources/#{source}", source: update_attrs)
assert redirected_to(conn) == ~p"/sources/#{source}"
conn = get(conn, ~p"/media_sources/sources/#{source}")
conn = get(conn, ~p"/sources/#{source}")
assert html_response(conn, 200) =~ "https://www.youtube.com/source/321xyz"
end
@ -85,7 +85,7 @@ defmodule PinchflatWeb.SourceControllerTest do
source: source,
invalid_attrs: invalid_attrs
} do
conn = put(conn, ~p"/media_sources/sources/#{source}", source: invalid_attrs)
conn = put(conn, ~p"/sources/#{source}", source: invalid_attrs)
assert html_response(conn, 200) =~ "Edit Source"
end
end
@ -94,11 +94,11 @@ defmodule PinchflatWeb.SourceControllerTest do
setup [:create_source]
test "deletes chosen source", %{conn: conn, source: source} do
conn = delete(conn, ~p"/media_sources/sources/#{source}")
assert redirected_to(conn) == ~p"/media_sources/sources"
conn = delete(conn, ~p"/sources/#{source}")
assert redirected_to(conn) == ~p"/sources"
assert_error_sent 404, fn ->
get(conn, ~p"/media_sources/sources/#{source}")
get(conn, ~p"/sources/#{source}")
end
end
end