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
View File
@@ -40,3 +40,4 @@ npm-debug.log
.env .env
.DS_Store .DS_Store
scratchpad.md scratchpad.md
/scratchpad/
+26
View File
@@ -2,4 +2,30 @@
@import 'tailwindcss/components'; @import 'tailwindcss/components';
@import 'tailwindcss/utilities'; @import 'tailwindcss/utilities';
@import './satoshi';
/* This file is for your main application CSS */ /* 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
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;
}
+12 -1
View File
@@ -21,10 +21,21 @@ import 'phoenix_html'
import { Socket } from 'phoenix' import { Socket } from 'phoenix'
import { LiveSocket } from 'phoenix_live_view' import { LiveSocket } from 'phoenix_live_view'
import topbar from '../vendor/topbar' 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 csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute('content')
let liveSocket = new LiveSocket('/live', Socket, { 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 // Show progress bar on live navigation and form submits
+4 -1
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"
}
} }
+274 -1
View File
@@ -2,15 +2,288 @@
// https://tailwindcss.com/docs/configuration // https://tailwindcss.com/docs/configuration
const plugin = require('tailwindcss/plugin') const plugin = require('tailwindcss/plugin')
const defaultTheme = require('tailwindcss/defaultTheme')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
module.exports = { module.exports = {
content: ['./js/**/*.js', '../lib/pinchflat_web.ex', '../lib/pinchflat_web/**/*.*ex'], content: ['./js/**/*.js', '../lib/pinchflat_web.ex', '../lib/pinchflat_web/**/*.*ex'],
darkMode: 'class',
theme: { theme: {
fontFamily: {
satoshi: ['Satoshi', 'sans-serif']
},
screens: {
'2xsm': '375px',
xsm: '425px',
'3xl': '2000px',
...defaultTheme.screens
},
extend: { extend: {
colors: { 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'
} }
} }
}, },
+18
View File
@@ -2,3 +2,21 @@
# yarn lockfile v1 # 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"
+2 -1
View File
@@ -27,7 +27,8 @@ defmodule Pinchflat.Profiles.MediaProfile do
schema "media_profiles" do schema "media_profiles" do
field :name, :string 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_subs, :boolean, default: true
field :download_auto_subs, :boolean, default: true field :download_auto_subs, :boolean, default: true
+3 -1
View File
@@ -84,8 +84,10 @@ defmodule PinchflatWeb do
# HTML escaping functionality # HTML escaping functionality
import Phoenix.HTML import Phoenix.HTML
# Core UI components and translation # Core UI components and translation
import PinchflatWeb.CoreComponents
import PinchflatWeb.Gettext import PinchflatWeb.Gettext
import PinchflatWeb.CoreComponents
import PinchflatWeb.CustomComponents.TableComponents
import PinchflatWeb.CustomComponents.ButtonComponents
# Shortcut for generating JS commands # Shortcut for generating JS commands
alias Phoenix.LiveView.JS alias Phoenix.LiveView.JS
+118 -94
View File
@@ -201,46 +201,14 @@ defmodule PinchflatWeb.CoreComponents do
def simple_form(assigns) do def simple_form(assigns) do
~H""" ~H"""
<.form :let={f} for={@for} as={@as} {@rest}> <.form :let={f} for={@for} as={@as} {@rest}>
<div class="mt-10 space-y-8 bg-white"> <%= render_slot(@inner_block, f) %>
<%= render_slot(@inner_block, f) %> <div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
<div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6"> <%= render_slot(action, f) %>
<%= render_slot(action, f) %>
</div>
</div> </div>
</.form> </.form>
""" """
end 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 """ @doc """
Renders an input with label and error messages. Renders an input with label and error messages.
@@ -270,11 +238,12 @@ defmodule PinchflatWeb.CoreComponents do
attr :name, :any attr :name, :any
attr :label, :string, default: nil attr :label, :string, default: nil
attr :value, :any attr :value, :any
attr :help, :string, default: nil
attr :type, :string, attr :type, :string,
default: "text", default: "text",
values: ~w(checkbox color date datetime-local email file hidden month number password 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, attr :field, Phoenix.HTML.FormField,
doc: "a form field struct retrieved from the form, for example: @form[:email]" doc: "a form field struct retrieved from the form, for example: @form[:email]"
@@ -308,7 +277,7 @@ defmodule PinchflatWeb.CoreComponents do
~H""" ~H"""
<div phx-feedback-for={@name}> <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="hidden" name={@name} value="false" />
<input <input
type="checkbox" type="checkbox"
@@ -316,16 +285,54 @@ defmodule PinchflatWeb.CoreComponents do
name={@name} name={@name}
value="true" value="true"
checked={@checked} checked={@checked}
class="rounded border-zinc-300 text-zinc-900 focus:ring-0" class="rounded focus:ring-0"
{@rest} {@rest}
/> />
<%= @label %> <%= @label %>
</label> </label>
<.help :if={@help}><%= @help %></.help>
<.error :for={msg <- @errors}><%= msg %></.error> <.error :for={msg <- @errors}><%= msg %></.error>
</div> </div>
""" """
end 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 def input(%{type: "select"} = assigns) do
~H""" ~H"""
<div phx-feedback-for={@name}> <div phx-feedback-for={@name}>
@@ -333,13 +340,17 @@ defmodule PinchflatWeb.CoreComponents do
<select <select
id={@id} id={@id}
name={@name} 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} multiple={@multiple}
{@rest} {@rest}
> >
<option :if={@prompt} value=""><%= @prompt %></option> <option :if={@prompt} value=""><%= @prompt %></option>
<%= Phoenix.HTML.Form.options_for_select(@options, @value) %> <%= Phoenix.HTML.Form.options_for_select(@options, @value) %>
</select> </select>
<.help :if={@help}><%= @help %></.help>
<.error :for={msg <- @errors}><%= msg %></.error> <.error :for={msg <- @errors}><%= msg %></.error>
</div> </div>
""" """
@@ -360,6 +371,7 @@ defmodule PinchflatWeb.CoreComponents do
]} ]}
{@rest} {@rest}
><%= Phoenix.HTML.Form.normalize_value("textarea", @value) %></textarea> ><%= Phoenix.HTML.Form.normalize_value("textarea", @value) %></textarea>
<.help :if={@help}><%= @help %></.help>
<.error :for={msg <- @errors}><%= msg %></.error> <.error :for={msg <- @errors}><%= msg %></.error>
</div> </div>
""" """
@@ -376,18 +388,32 @@ defmodule PinchflatWeb.CoreComponents do
id={@id} id={@id}
value={Phoenix.HTML.Form.normalize_value(@type, @value)} value={Phoenix.HTML.Form.normalize_value(@type, @value)}
class={[ class={[
"mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6", "w-full rounded-lg border-[1.5px] border-stroke bg-transparent px-5 py-3 font-normal text-black",
"phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400", "outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter",
@errors == [] && "border-zinc-300 focus:border-zinc-400", "dark:border-form-strokedark dark:bg-form-input dark:text-white dark:focus:border-primary",
@errors != [] && "border-rose-400 focus:border-rose-400" @errors != [] && "border-rose-400 focus:border-rose-400"
]} ]}
{@rest} {@rest}
/> />
<.help :if={@help}><%= @help %></.help>
<.error :for={msg <- @errors}><%= msg %></.error> <.error :for={msg <- @errors}><%= msg %></.error>
</div> </div>
""" """
end 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 """ @doc """
Renders a label. Renders a label.
""" """
@@ -396,7 +422,7 @@ defmodule PinchflatWeb.CoreComponents do
def label(assigns) do def label(assigns) do
~H""" ~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) %> <%= render_slot(@inner_block) %>
</label> </label>
""" """
@@ -409,7 +435,7 @@ defmodule PinchflatWeb.CoreComponents do
def error(assigns) do def error(assigns) do
~H""" ~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" /> <.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" />
<%= render_slot(@inner_block) %> <%= render_slot(@inner_block) %>
</p> </p>
@@ -425,7 +451,7 @@ defmodule PinchflatWeb.CoreComponents do
slot :subtitle slot :subtitle
slot :actions slot :actions
def header(assigns) do def old_header(assigns) do
~H""" ~H"""
<header class={[@actions != [] && "flex items-center justify-between gap-6", @class]}> <header class={[@actions != [] && "flex items-center justify-between gap-6", @class]}>
<div> <div>
@@ -446,10 +472,10 @@ defmodule PinchflatWeb.CoreComponents do
## Examples ## Examples
<.table id="users" rows={@users}> <.old_table id="users" rows={@users}>
<:col :let={user} label="id"><%= user.id %></:col> <:col :let={user} label="id"><%= user.id %></:col>
<:col :let={user} label="username"><%= user.username %></:col> <:col :let={user} label="username"><%= user.username %></:col>
</.table> </.old_table>
""" """
attr :id, :string, required: true attr :id, :string, required: true
attr :rows, :list, 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" slot :action, doc: "the slot for showing user actions in the last table column"
def table(assigns) do def old_table(assigns) do
assigns = assigns =
with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end) assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end)
end end
~H""" ~H"""
<div class="overflow-y-auto px-4 sm:overflow-visible sm:px-0"> <table class="w-[40rem] mt-11 sm:w-full">
<table class="w-[40rem] mt-11 sm:w-full"> <thead class="text-sm text-left leading-6 text-zinc-500">
<thead class="text-sm text-left leading-6 text-zinc-500"> <tr>
<tr> <th :for={col <- @col} class="p-0 pb-4 pr-6 font-normal"><%= col[:label] %></th>
<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">
<th :if={@action != []} class="relative p-0 pb-4"> <span class="sr-only"><%= gettext("Actions") %></span>
<span class="sr-only"><%= gettext("Actions") %></span> </th>
</th> </tr>
</tr> </thead>
</thead> <tbody
<tbody id={@id}
id={@id} phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}
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"
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">
<tr :for={row <- @rows} id={@row_id && @row_id.(row)} class="group hover:bg-zinc-50"> <td
<td :for={{col, i} <- Enum.with_index(@col)}
:for={{col, i} <- Enum.with_index(@col)} phx-click={@row_click && @row_click.(row)}
phx-click={@row_click && @row_click.(row)} class={["relative p-0", @row_click && "hover:cursor-pointer"]}
class={["relative p-0", @row_click && "hover:cursor-pointer"]} >
> <div class="block py-4 pr-6">
<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="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"]}>
<span class={["relative", i == 0 && "font-semibold text-zinc-900"]}> <%= render_slot(col, @row_item.(row)) %>
<%= render_slot(col, @row_item.(row)) %> </span>
</span> </div>
</div> </td>
</td> <td :if={@action != []} class="relative w-14 p-0">
<td :if={@action != []} class="relative w-14 p-0"> <div class="relative whitespace-nowrap py-4 text-right text-sm font-medium">
<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 class="absolute -inset-y-px -right-4 left-0 group-hover:bg-zinc-50 sm:rounded-r-xl" /> <span
<span :for={action <- @action}
:for={action <- @action} class="relative ml-4 font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
class="relative ml-4 font-semibold leading-6 text-zinc-900 hover:text-zinc-700" >
> <%= render_slot(action, @row_item.(row)) %>
<%= render_slot(action, @row_item.(row)) %> </span>
</span> </div>
</div> </td>
</td> </tr>
</tr> </tbody>
</tbody> </table>
</table>
</div>
""" """
end end
@@ -536,10 +560,10 @@ defmodule PinchflatWeb.CoreComponents do
def list(assigns) do def list(assigns) do
~H""" ~H"""
<div class="mt-2 mb-14"> <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"> <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> <dt class="w-1/4 flex-none dark:text-white"><%= item.title %></dt>
<dd class="text-zinc-700"><%= render_slot(item) %></dd> <dd class="dark:text-white"><%= render_slot(item) %></dd>
</div> </div>
</dl> </dl>
</div> </div>
@@ -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
@@ -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
+24
View File
@@ -2,4 +2,28 @@ defmodule PinchflatWeb.Layouts do
use PinchflatWeb, :html use PinchflatWeb, :html
embed_templates "layouts/*" 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 end
@@ -1,32 +1,13 @@
<header class="px-4 sm:px-6 lg:px-8"> <div class="flex h-screen overflow-hidden">
<div class="flex items-center justify-between border-b border-zinc-100 py-3 text-sm"> <.sidebar />
<div class="flex items-center gap-4">
<a href="/"> <div class="relative flex flex-1 flex-col overflow-y-auto overflow-x-hidden">
<img src={~p"/images/logo.svg"} width="36" /> <.header />
</a> <main>
<p class="bg-brand/5 text-brand rounded-full px-2 font-medium leading-6"> <div class="mx-auto max-w-screen-2xl p-4 md:p-6 2xl:p-10">
v<%= Application.spec(:phoenix, :vsn) %> <.flash_group flash={@flash} />
</p> <%= @inner_content %>
</div> </div>
<div class="flex items-center gap-4 font-semibold leading-6 text-zinc-900"> </main>
<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> </div>
</header> </div>
<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>
@@ -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>
@@ -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>
@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" class="[scrollbar-gutter:stable]"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <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 defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
</script> </script>
</head> </head>
<body class="bg-white antialiased"> <body x-data="{ 'sidebarToggle': false }" class="dark text-bodydark bg-boxdark-2">
<%= @inner_content %> <%= @inner_content %>
</body> </body>
</html> </html>
@@ -1,8 +1,14 @@
<.header> <div class="mb-6 flex gap-3 flex-row items-center">
Edit Media profile <%= @media_profile.id %> <.link navigate={~p"/media_profiles"}>
<:subtitle>Use this form to manage media_profile records in your database.</:subtitle> <.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
</.header> </.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}"} /> <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">
<.back navigate={~p"/media_profiles"}>Back to media_profiles</.back> <div class="flex flex-col gap-10">
<.media_profile_form changeset={@changeset} action={~p"/media_profiles/#{@media_profile}"} />
</div>
</div>
</div>
@@ -1,30 +1,39 @@
<.header> <div class="mb-6 flex gap-3 flex-row items-center justify-between">
Listing Media profiles <h2 class="text-title-md2 font-bold text-black dark:text-white">All Media Profiles</h2>
<:actions> <nav>
<.link href={~p"/media_profiles/new"}> <.link navigate={~p"/media_profiles/new"}>
<.button>New Media profile</.button> <.button color="bg-primary" rounding="rounded-full">
<span class="font-bold mx-2">+</span> New Media Profile
</.button>
</.link> </.link>
</:actions> </nav>
</.header> </div>
<.table <div class="rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark">
id="media_profiles" <div class="max-w-full overflow-x-auto">
rows={@media_profiles} <div class="flex flex-col gap-10">
row_click={&JS.navigate(~p"/media_profiles/#{&1}")} <.table rows={@media_profiles} table_class="text-black dark:text-white">
> <:col :let={media_profile} label="Name">
<:col :let={media_profile} label="Name"><%= media_profile.name %></:col> <%= media_profile.name %>
<:col :let={media_profile} label="Output path template"> </:col>
<%= media_profile.output_path_template %> <:col :let={media_profile} label="Output Template">
</:col> <code class="text-sm"><%= media_profile.output_path_template %></code>
<:action :let={media_profile}> </:col>
<div class="sr-only"> <:col :let={media_profile} label="" class="flex place-content-evenly">
<.link navigate={~p"/media_profiles/#{media_profile}"}>Show</.link> <.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> </div>
<.link navigate={~p"/media_profiles/#{media_profile}/edit"}>Edit</.link> </div>
</:action> </div>
<:action :let={media_profile}>
<.link href={~p"/media_profiles/#{media_profile}"} method="delete" data-confirm="Are you sure?">
Delete
</.link>
</:action>
</.table>
@@ -2,29 +2,62 @@
<.error :if={@changeset.action}> <.error :if={@changeset.action}>
Oops, something went wrong! Please check the errors below. Oops, something went wrong! Please check the errors below.
</.error> </.error>
<.input field={f[:name]} type="text" label="Name" /> <h3 class="my-4 text-2xl text-black dark:text-white">
<.input field={f[:output_path_template]} type="text" label="Output path template" /> 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> <h3 class="mb-4 mt-8 text-2xl text-black dark:text-white">
<.input field={f[:download_subs]} type="checkbox" label="Download Subs" /> Subtitle Options
<.input field={f[:download_auto_subs]} type="checkbox" label="Download Autogenerated Subs" /> </h3>
<.input field={f[:embed_subs]} type="checkbox" label="Embed Subs" /> <.input field={f[:download_subs]} type="toggle" label="Download Subtitles" />
<.input field={f[:sub_langs]} type="text" label="Sub Langs" /> <.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> <h3 class="mb-4 mt-8 text-2xl text-black dark:text-white">
<.input field={f[:download_thumbnail]} type="checkbox" label="Download Thumbnail" /> Thumbnail Options
<.input field={f[:embed_thumbnail]} type="checkbox" label="Embed Thumbnail" /> </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> <h3 class="mb-4 mt-8 text-2xl text-black dark:text-white">
<.input field={f[:download_metadata]} type="checkbox" label="Download Metadata" /> Metadata Options
<.input field={f[:embed_metadata]} type="checkbox" label="Embed Metadata" /> </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 <.input
field={f[:shorts_behaviour]} field={f[:shorts_behaviour]}
options={friendly_format_type_options()} options={friendly_format_type_options()}
type="select" type="select"
label="Include Shorts?" label="Include Shorts?"
help="Experimental"
/> />
<.input <.input
field={f[:livestream_behaviour]} field={f[:livestream_behaviour]}
@@ -34,6 +67,6 @@
/> />
<:actions> <:actions>
<.button>Save Media profile</.button> <.button class="mt-15 mb-5 sm:mb-7.5">Save Media profile</.button>
</:actions> </:actions>
</.simple_form> </.simple_form>
@@ -1,8 +1,14 @@
<.header> <div class="mb-6 flex gap-3 flex-row items-center">
New Media profile <.link navigate={~p"/media_profiles"}>
<:subtitle>Use this form to manage media_profile records in your database.</:subtitle> <.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
</.header> </.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"} /> <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">
<.back navigate={~p"/media_profiles"}>Back to media_profiles</.back> <div class="flex flex-col gap-10">
<.media_profile_form changeset={@changeset} action={~p"/media_profiles"} />
</div>
</div>
</div>
@@ -1,13 +1,26 @@
<.header> <div class="mb-6 flex gap-3 flex-row items-center justify-between">
Media profile <%= @media_profile.id %> <div class="flex gap-3 items-center">
<:subtitle>This is a media_profile record from your database.</:subtitle> <.link navigate={~p"/media_profiles"}>
<:actions> <.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
<.link href={~p"/media_profiles/#{@media_profile}/edit"}>
<.button>Edit media_profile</.button>
</.link> </.link>
</:actions> <h2 class="text-title-md2 font-bold text-black dark:text-white ml-4">
</.header> Media Profile #<%= @media_profile.id %>
</h2>
</div>
<.list_items_from_map map={Map.from_struct(@media_profile)} /> <nav>
<.link navigate={~p"/media_profiles/#{@media_profile}/edit"}>
<.back navigate={~p"/media_profiles"}>Back to media_profiles</.back> <.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>
@@ -7,7 +7,7 @@ defmodule PinchflatWeb.MediaSources.SourceController do
alias Pinchflat.MediaSource.Source alias Pinchflat.MediaSource.Source
def index(conn, _params) do def index(conn, _params) do
sources = MediaSource.list_sources() sources = Repo.preload(MediaSource.list_sources(), :media_profile)
render(conn, :index, sources: sources) render(conn, :index, sources: sources)
end end
@@ -23,7 +23,7 @@ defmodule PinchflatWeb.MediaSources.SourceController do
{:ok, source} -> {:ok, source} ->
conn conn
|> put_flash(:info, "Source created successfully.") |> put_flash(:info, "Source created successfully.")
|> redirect(to: ~p"/media_sources/sources/#{source}") |> redirect(to: ~p"/sources/#{source}")
{:error, %Ecto.Changeset{} = changeset} -> {:error, %Ecto.Changeset{} = changeset} ->
render(conn, :new, changeset: changeset, media_profiles: media_profiles()) render(conn, :new, changeset: changeset, media_profiles: media_profiles())
@@ -53,7 +53,7 @@ defmodule PinchflatWeb.MediaSources.SourceController do
{:ok, source} -> {:ok, source} ->
conn conn
|> put_flash(:info, "Source updated successfully.") |> put_flash(:info, "Source updated successfully.")
|> redirect(to: ~p"/media_sources/sources/#{source}") |> redirect(to: ~p"/sources/#{source}")
{:error, %Ecto.Changeset{} = changeset} -> {:error, %Ecto.Changeset{} = changeset} ->
render(conn, :edit, render(conn, :edit,
@@ -70,7 +70,7 @@ defmodule PinchflatWeb.MediaSources.SourceController do
conn conn
|> put_flash(:info, "Source deleted successfully.") |> put_flash(:info, "Source deleted successfully.")
|> redirect(to: ~p"/media_sources/sources") |> redirect(to: ~p"/sources")
end end
defp media_profiles do defp media_profiles do
@@ -1,12 +1,18 @@
<.header> <div class="mb-6 flex gap-3 flex-row items-center">
Edit Source <%= @source.id %> <.link navigate={~p"/sources"}>
<:subtitle>Use this form to manage source records in your database.</:subtitle> <.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
</.header> </.link>
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4">Edit Source</h2>
</div>
<.source_form <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">
changeset={@changeset} <div class="max-w-full overflow-x-auto">
media_profiles={@media_profiles} <div class="flex flex-col gap-10">
action={~p"/media_sources/sources/#{@source}"} <.source_form
/> changeset={@changeset}
media_profiles={@media_profiles}
<.back navigate={~p"/media_sources/sources"}>Back to sources</.back> action={~p"/sources/#{@source}"}
/>
</div>
</div>
</div>
@@ -1,24 +1,48 @@
<.header> <div class="mb-6 flex gap-3 flex-row items-center justify-between">
Listing Sources <h2 class="text-title-md2 font-bold text-black dark:text-white">All Sources</h2>
<:actions> <nav>
<.link href={~p"/media_sources/sources/new"}> <.link navigate={~p"/sources/new"}>
<.button>New Source</.button> <.button color="bg-primary" rounding="rounded-full">
<span class="font-bold mx-2">+</span> New Source
</.button>
</.link> </.link>
</:actions> </nav>
</.header> </div>
<.table id="sources" rows={@sources} row_click={&JS.navigate(~p"/media_sources/sources/#{&1}")}> <div class="rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark">
<:col :let={source} label="Collection Name"><%= source.collection_name %></:col> <div class="max-w-full overflow-x-auto">
<:col :let={source} label="Collection ID"><%= source.collection_id %></:col> <div class="flex flex-col gap-10">
<:action :let={source}> <.table rows={@sources} table_class="text-black dark:text-white">
<div class="sr-only"> <:col :let={source} label="Name">
<.link navigate={~p"/media_sources/sources/#{source}"}>Show</.link> <%= 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> </div>
<.link navigate={~p"/media_sources/sources/#{source}/edit"}>Edit</.link> </div>
</:action> </div>
<:action :let={source}>
<.link href={~p"/media_sources/sources/#{source}"} method="delete" data-confirm="Are you sure?">
Delete
</.link>
</:action>
</.table>
@@ -1,12 +1,14 @@
<.header> <div class="mb-6 flex gap-3 flex-row items-center">
New Source <.link navigate={~p"/sources"}>
<:subtitle>Use this form to manage source records in your database.</:subtitle> <.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
</.header> </.link>
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4">New Source</h2>
</div>
<.source_form <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">
changeset={@changeset} <div class="max-w-full overflow-x-auto">
media_profiles={@media_profiles} <div class="flex flex-col gap-10">
action={~p"/media_sources/sources"} <.source_form changeset={@changeset} media_profiles={@media_profiles} action={~p"/sources"} />
/> </div>
</div>
<.back navigate={~p"/media_sources/sources"}>Back to sources</.back> </div>
@@ -1,23 +1,38 @@
<.header> <div class="mb-6 flex gap-3 flex-row items-center justify-between">
Source <%= @source.id %> <div class="flex gap-3 items-center">
<:subtitle>This is a source record from your database.</:subtitle> <.link navigate={~p"/sources"}>
<:actions> <.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
<.link href={~p"/media_sources/sources/#{@source}/edit"}>
<.button>Edit source</.button>
</.link> </.link>
</:actions> <h2 class="text-title-md2 font-bold text-black dark:text-white ml-4">
</.header> Source #<%= @source.id %>
</h2>
</div>
<h3 class="mt-14">Relationships</h3> <nav>
<.list> <.link navigate={~p"/sources/#{@source}/edit"}>
<:item title="media_profile"> <.button color="bg-primary" rounding="rounded-full">
<.link href={~p"/media_profiles/#{@source.media_profile}"}> <.icon name="hero-pencil-square" class="mr-2" /> Edit Source
<%= @source.media_profile.name %> </.button>
</.link> </.link>
</:item> </nav>
</.list> </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> <h3 class="font-bold text-xl">Attributes</h3>
<.list_items_from_map map={Map.from_struct(@source)} /> <.list_items_from_map map={Map.from_struct(@source)} />
</div>
<.back navigate={~p"/media_sources/sources"}>Back to sources</.back> </div>
</div>
@@ -3,6 +3,13 @@
Oops, something went wrong! Please check the errors below. Oops, something went wrong! Please check the errors below.
</.error> </.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 field={f[:friendly_name]} type="text" label="Friendly Name" />
<.input <.input
@@ -16,21 +23,25 @@
field={f[:collection_type]} field={f[:collection_type]}
options={friendly_collection_types()} options={friendly_collection_types()}
type="select" type="select"
label="Collection Type" label="Source Type"
/> />
<.input field={f[:original_url]} type="text" label="Source URL" />
<.input <.input
field={f[:index_frequency_minutes]} field={f[:index_frequency_minutes]}
options={friendly_index_frequencies()} options={friendly_index_frequencies()}
type="select" type="select"
label="Index Frequency" 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> <:actions>
<.button>Save Source</.button> <.button class="mt-15 mb-5 sm:mb-7.5">Save Source</.button>
</:actions> </:actions>
</.simple_form> </.simple_form>
@@ -4,6 +4,6 @@ defmodule PinchflatWeb.PageController do
def home(conn, _params) do def home(conn, _params) do
# The home page is often custom made, # The home page is often custom made,
# so skip the default app layout. # so skip the default app layout.
render(conn, :home, layout: false) render(conn, :home)
end end
end end
+1 -4
View File
@@ -20,10 +20,7 @@ defmodule PinchflatWeb.Router do
get "/", PageController, :home get "/", PageController, :home
resources "/media_profiles", MediaProfiles.MediaProfileController resources "/media_profiles", MediaProfiles.MediaProfileController
resources "/sources", MediaSources.SourceController
scope "/media_sources", MediaSources do
resources "/sources", SourceController
end
end end
# Other scopes may use custom stacks. # 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.
@@ -13,14 +13,14 @@ defmodule PinchflatWeb.MediaProfileControllerTest do
describe "index" do describe "index" do
test "lists all media_profiles", %{conn: conn} do test "lists all media_profiles", %{conn: conn} do
conn = get(conn, ~p"/media_profiles") conn = get(conn, ~p"/media_profiles")
assert html_response(conn, 200) =~ "Listing Media profiles" assert html_response(conn, 200) =~ "All Media Profiles"
end end
end end
describe "new media_profile" do describe "new media_profile" do
test "renders form", %{conn: conn} do test "renders form", %{conn: conn} do
conn = get(conn, ~p"/media_profiles/new") 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
end end
@@ -32,12 +32,12 @@ defmodule PinchflatWeb.MediaProfileControllerTest do
assert redirected_to(conn) == ~p"/media_profiles/#{id}" assert redirected_to(conn) == ~p"/media_profiles/#{id}"
conn = get(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 end
test "renders errors when data is invalid", %{conn: conn} do test "renders errors when data is invalid", %{conn: conn} do
conn = post(conn, ~p"/media_profiles", media_profile: @invalid_attrs) 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
end end
@@ -49,7 +49,7 @@ defmodule PinchflatWeb.MediaProfileControllerTest do
media_profile: media_profile media_profile: media_profile
} do } do
conn = get(conn, ~p"/media_profiles/#{media_profile}/edit") 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
end end
@@ -66,7 +66,7 @@ defmodule PinchflatWeb.MediaProfileControllerTest do
test "renders errors when data is invalid", %{conn: conn, media_profile: media_profile} 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) 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
end end
@@ -28,14 +28,14 @@ defmodule PinchflatWeb.SourceControllerTest do
describe "index" do describe "index" do
test "lists all sources", %{conn: conn} do test "lists all sources", %{conn: conn} do
conn = get(conn, ~p"/media_sources/sources") conn = get(conn, ~p"/sources")
assert html_response(conn, 200) =~ "Listing Sources" assert html_response(conn, 200) =~ "All Sources"
end end
end end
describe "new source" do describe "new source" do
test "renders form", %{conn: conn} 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" assert html_response(conn, 200) =~ "New Source"
end end
end end
@@ -43,17 +43,17 @@ defmodule PinchflatWeb.SourceControllerTest do
describe "create source" do describe "create source" do
test "redirects to show when data is valid", %{conn: conn, create_attrs: create_attrs} do test "redirects to show when data is valid", %{conn: conn, create_attrs: create_attrs} do
expect(YtDlpRunnerMock, :run, 1, &runner_function_mock/3) 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 %{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}") conn = get(conn, ~p"/sources/#{id}")
assert html_response(conn, 200) =~ "Source #{id}" assert html_response(conn, 200) =~ "Source ##{id}"
end end
test "renders errors when data is invalid", %{conn: conn, invalid_attrs: invalid_attrs} do 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" assert html_response(conn, 200) =~ "New Source"
end end
end end
@@ -62,7 +62,7 @@ defmodule PinchflatWeb.SourceControllerTest do
setup [:create_source] setup [:create_source]
test "renders form for editing chosen source", %{conn: conn, source: source} do 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" assert html_response(conn, 200) =~ "Edit Source"
end end
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 test "redirects when data is valid", %{conn: conn, source: source, update_attrs: update_attrs} do
expect(YtDlpRunnerMock, :run, 1, &runner_function_mock/3) expect(YtDlpRunnerMock, :run, 1, &runner_function_mock/3)
conn = put(conn, ~p"/media_sources/sources/#{source}", source: update_attrs) conn = put(conn, ~p"/sources/#{source}", source: update_attrs)
assert redirected_to(conn) == ~p"/media_sources/sources/#{source}" 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" assert html_response(conn, 200) =~ "https://www.youtube.com/source/321xyz"
end end
@@ -85,7 +85,7 @@ defmodule PinchflatWeb.SourceControllerTest do
source: source, source: source,
invalid_attrs: invalid_attrs invalid_attrs: invalid_attrs
} do } 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" assert html_response(conn, 200) =~ "Edit Source"
end end
end end
@@ -94,11 +94,11 @@ defmodule PinchflatWeb.SourceControllerTest do
setup [:create_source] setup [:create_source]
test "deletes chosen source", %{conn: conn, source: source} do test "deletes chosen source", %{conn: conn, source: source} do
conn = delete(conn, ~p"/media_sources/sources/#{source}") conn = delete(conn, ~p"/sources/#{source}")
assert redirected_to(conn) == ~p"/media_sources/sources" assert redirected_to(conn) == ~p"/sources"
assert_error_sent 404, fn -> assert_error_sent 404, fn ->
get(conn, ~p"/media_sources/sources/#{source}") get(conn, ~p"/sources/#{source}")
end end
end end
end end