├── rel
├── overlays
│ └── bin
│ │ ├── server.bat
│ │ ├── migrate.bat
│ │ ├── server
│ │ └── migrate
└── env.sh.eex
├── test
├── test_helper.exs
├── glitchtv_web
│ ├── controllers
│ │ ├── page_controller_test.exs
│ │ ├── error_json_test.exs
│ │ └── error_html_test.exs
│ └── live
│ │ └── recording_live_test.exs
├── support
│ ├── fixtures
│ │ └── recordings_fixtures.ex
│ ├── conn_case.ex
│ └── data_case.ex
└── glitchtv
│ └── recordings_test.exs
├── priv
├── repo
│ ├── migrations
│ │ ├── .formatter.exs
│ │ └── 20250212122810_create_recordings.exs
│ └── seeds.exs
└── static
│ ├── favicon.ico
│ ├── images
│ ├── swm-logo.png
│ ├── github-logo.svg
│ ├── github-dark-mode-logo.svg
│ ├── swm-white-logo.svg
│ ├── elixir-webrtc-logo.svg
│ ├── elixir-webrtc-dark-mode-logo.svg
│ ├── swm-dark-mode-logo.svg
│ └── swm-logo.svg
│ └── robots.txt
├── lib
├── glitchtv
│ ├── repo.ex
│ ├── release.ex
│ ├── recordings
│ │ └── recording.ex
│ ├── application.ex
│ ├── stream_service.ex
│ ├── recordings_service.ex
│ └── recordings.ex
├── glitchtv.ex
├── glitchtv_web
│ ├── controllers
│ │ ├── page_controller.ex
│ │ ├── page_html.ex
│ │ ├── error_json.ex
│ │ ├── error_html.ex
│ │ └── page_html
│ │ │ └── home.html.heex
│ ├── channels
│ │ └── presence.ex
│ ├── components
│ │ ├── layouts.ex
│ │ ├── layouts
│ │ │ ├── root.html.heex
│ │ │ └── app.html.heex
│ │ └── core_components.ex
│ ├── live
│ │ ├── recording_live
│ │ │ ├── show.ex
│ │ │ ├── index.ex
│ │ │ ├── index.html.heex
│ │ │ └── show.html.heex
│ │ ├── stream_viewer_live.ex
│ │ ├── chat_live.ex
│ │ └── streamer_live.ex
│ ├── router.ex
│ ├── endpoint.ex
│ └── telemetry.ex
└── glitchtv_web.ex
├── assets
├── js
│ ├── ScrollDownHook.js
│ ├── ShareButtonHook.js
│ ├── DarkModeToggleHook.js
│ └── app.js
├── css
│ └── app.css
├── tailwind.config.js
└── vendor
│ └── topbar.js
├── .formatter.exs
├── litestream.sh
├── config
├── litestream.yml
├── prod.exs
├── test.exs
├── config.exs
├── dev.exs
└── runtime.exs
├── .github
└── workflows
│ └── fly-deploy.yml
├── README.md
├── fly.toml
├── .gitignore
├── .dockerignore
├── mix.exs
├── Dockerfile
└── mix.lock
/rel/overlays/bin/server.bat:
--------------------------------------------------------------------------------
1 | set PHX_SERVER=true
2 | call "%~dp0\glitchtv" start
3 |
--------------------------------------------------------------------------------
/rel/overlays/bin/migrate.bat:
--------------------------------------------------------------------------------
1 | call "%~dp0\glitchtv" eval Glitchtv.Release.migrate
2 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 | Ecto.Adapters.SQL.Sandbox.mode(Glitchtv.Repo, :manual)
3 |
--------------------------------------------------------------------------------
/priv/repo/migrations/.formatter.exs:
--------------------------------------------------------------------------------
1 | [
2 | import_deps: [:ecto_sql],
3 | inputs: ["*.exs"]
4 | ]
5 |
--------------------------------------------------------------------------------
/priv/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elixir-webrtc/glitchtv-demo/HEAD/priv/static/favicon.ico
--------------------------------------------------------------------------------
/priv/static/images/swm-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elixir-webrtc/glitchtv-demo/HEAD/priv/static/images/swm-logo.png
--------------------------------------------------------------------------------
/rel/overlays/bin/server:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -eu
3 |
4 | cd -P -- "$(dirname -- "$0")"
5 | PHX_SERVER=true exec ./glitchtv start
6 |
--------------------------------------------------------------------------------
/rel/overlays/bin/migrate:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -eu
3 |
4 | cd -P -- "$(dirname -- "$0")"
5 | exec ./glitchtv eval Glitchtv.Release.migrate
6 |
--------------------------------------------------------------------------------
/lib/glitchtv/repo.ex:
--------------------------------------------------------------------------------
1 | defmodule Glitchtv.Repo do
2 | use Ecto.Repo,
3 | otp_app: :glitchtv,
4 | adapter: Ecto.Adapters.SQLite3
5 | end
6 |
--------------------------------------------------------------------------------
/assets/js/ScrollDownHook.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @type {import("phoenix_live_view").ViewHookInterface}
3 | */
4 | export default {
5 | mounted() {
6 | this.el.scrollTo(0, this.el.scrollHeight);
7 | },
8 | };
9 |
--------------------------------------------------------------------------------
/priv/static/robots.txt:
--------------------------------------------------------------------------------
1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/.formatter.exs:
--------------------------------------------------------------------------------
1 | [
2 | import_deps: [:ecto, :ecto_sql, :phoenix],
3 | subdirectories: ["priv/*/migrations"],
4 | plugins: [Phoenix.LiveView.HTMLFormatter],
5 | inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"]
6 | ]
7 |
--------------------------------------------------------------------------------
/test/glitchtv_web/controllers/page_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule GlitchtvWeb.PageControllerTest do
2 | use GlitchtvWeb.ConnCase
3 |
4 | test "GET /", %{conn: conn} do
5 | conn = get(conn, ~p"/")
6 | assert html_response(conn, 200) =~ "Peace of mind from prototype to production"
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/glitchtv.ex:
--------------------------------------------------------------------------------
1 | defmodule Glitchtv do
2 | @moduledoc """
3 | Glitchtv keeps the contexts that define your domain
4 | and business logic.
5 |
6 | Contexts are also responsible for managing your data, regardless
7 | if it comes from the database, an external API or others.
8 | """
9 | end
10 |
--------------------------------------------------------------------------------
/lib/glitchtv_web/controllers/page_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule GlitchtvWeb.PageController do
2 | use GlitchtvWeb, :controller
3 |
4 | def home(conn, _params) do
5 | # The home page is often custom made,
6 | # so skip the default app layout.
7 | render(conn, :home, layout: false)
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/glitchtv_web/controllers/page_html.ex:
--------------------------------------------------------------------------------
1 | defmodule GlitchtvWeb.PageHTML do
2 | @moduledoc """
3 | This module contains pages rendered by PageController.
4 |
5 | See the `page_html` directory for all templates available.
6 | """
7 | use GlitchtvWeb, :html
8 |
9 | embed_templates "page_html/*"
10 | end
11 |
--------------------------------------------------------------------------------
/lib/glitchtv_web/channels/presence.ex:
--------------------------------------------------------------------------------
1 | defmodule GlitchtvWeb.Presence do
2 | @moduledoc """
3 | Provides presence tracking to channels and processes.
4 |
5 | See the [`Phoenix.Presence`](https://hexdocs.pm/phoenix/Phoenix.Presence.html)
6 | docs for more details.
7 | """
8 | use Phoenix.Presence,
9 | otp_app: :glitchtv,
10 | pubsub_server: Glitchtv.PubSub
11 | end
12 |
--------------------------------------------------------------------------------
/priv/repo/seeds.exs:
--------------------------------------------------------------------------------
1 | # Script for populating the database. You can run it as:
2 | #
3 | # mix run priv/repo/seeds.exs
4 | #
5 | # Inside the script, you can read and write to any of your
6 | # repositories directly:
7 | #
8 | # Glitchtv.Repo.insert!(%Glitchtv.SomeSchema{})
9 | #
10 | # We recommend using the bang functions (`insert!`, `update!`
11 | # and so on) as they will fail if something goes wrong.
12 |
--------------------------------------------------------------------------------
/litestream.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -e
3 |
4 | # If db doesn't exist, try restoring from object storage
5 | if [ ! -f "$DATABASE_PATH" ] && [ -n "$BUCKET_NAME" ]; then
6 | litestream restore -if-replica-exists "$DATABASE_PATH"
7 | fi
8 |
9 | # Migrate database
10 | /app/bin/migrate
11 |
12 | # Launch application
13 | if [ -n "$BUCKET_NAME" ]; then
14 | litestream replicate -exec "${*}"
15 | else
16 | exec "${@}"
17 | fi
--------------------------------------------------------------------------------
/config/litestream.yml:
--------------------------------------------------------------------------------
1 | # This is the configuration file for litestream.
2 | #
3 | # For more details, see: https://litestream.io/reference/config/
4 | #
5 | dbs:
6 | - path: $DATABASE_PATH
7 | replicas:
8 | - type: s3
9 | endpoint: $AWS_ENDPOINT_URL_S3
10 | bucket: $BUCKET_NAME
11 | path: litestream${DATABASE_PATH}
12 | access-key-id: $AWS_ACCESS_KEY_ID
13 | secret-access-key: $AWS_SECRET_ACCESS_KEY
14 | region: $AWS_REGION
--------------------------------------------------------------------------------
/test/glitchtv_web/controllers/error_json_test.exs:
--------------------------------------------------------------------------------
1 | defmodule GlitchtvWeb.ErrorJSONTest do
2 | use GlitchtvWeb.ConnCase, async: true
3 |
4 | test "renders 404" do
5 | assert GlitchtvWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}}
6 | end
7 |
8 | test "renders 500" do
9 | assert GlitchtvWeb.ErrorJSON.render("500.json", %{}) ==
10 | %{errors: %{detail: "Internal Server Error"}}
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/assets/js/ShareButtonHook.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @type {import("phoenix_live_view").ViewHookInterface}
3 | */
4 | export default {
5 | mounted() {
6 | this.el.addEventListener("click", async () => {
7 | await navigator.clipboard.writeText(window.location.href);
8 |
9 | const previous = this.el.innerHTML;
10 | this.el.innerHTML = "Copied!";
11 |
12 | setTimeout(() => (this.el.innerHTML = previous), 2000);
13 | });
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/test/glitchtv_web/controllers/error_html_test.exs:
--------------------------------------------------------------------------------
1 | defmodule GlitchtvWeb.ErrorHTMLTest do
2 | use GlitchtvWeb.ConnCase, async: true
3 |
4 | # Bring render_to_string/4 for testing custom views
5 | import Phoenix.Template
6 |
7 | test "renders 404.html" do
8 | assert render_to_string(GlitchtvWeb.ErrorHTML, "404", "html", []) == "Not Found"
9 | end
10 |
11 | test "renders 500.html" do
12 | assert render_to_string(GlitchtvWeb.ErrorHTML, "500", "html", []) == "Internal Server Error"
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20250212122810_create_recordings.exs:
--------------------------------------------------------------------------------
1 | defmodule Glitchtv.Repo.Migrations.CreateRecordings do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:recordings) do
6 | add :title, :string
7 | add :description, :string
8 | add :link, :string
9 | add :thumbnail_link, :string
10 | add :length_seconds, :integer
11 | add :date, :utc_datetime
12 | add :views_count, :integer
13 |
14 | timestamps(type: :utc_datetime)
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/glitchtv_web/components/layouts.ex:
--------------------------------------------------------------------------------
1 | defmodule GlitchtvWeb.Layouts do
2 | @moduledoc """
3 | This module holds different layouts used by your application.
4 |
5 | See the `layouts` directory for all templates available.
6 | The "root" layout is a skeleton rendered as part of the
7 | application router. The "app" layout is set as the default
8 | layout on both `use GlitchtvWeb, :controller` and
9 | `use GlitchtvWeb, :live_view`.
10 | """
11 | use GlitchtvWeb, :html
12 |
13 | embed_templates "layouts/*"
14 | end
15 |
--------------------------------------------------------------------------------
/rel/env.sh.eex:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # configure node for distributed erlang with IPV6 support
4 | export ERL_AFLAGS="-proto_dist inet6_tcp"
5 | export ECTO_IPV6="true"
6 | export DNS_CLUSTER_QUERY="${FLY_APP_NAME}.internal"
7 | export RELEASE_DISTRIBUTION="name"
8 | export RELEASE_NODE="${FLY_APP_NAME}-${FLY_IMAGE_REF##*-}@${FLY_PRIVATE_IP}"
9 |
10 | # Uncomment to send crash dumps to stderr
11 | # This can be useful for debugging, but may log sensitive information
12 | # export ERL_CRASH_DUMP=/dev/stderr
13 | # export ERL_CRASH_DUMP_BYTES=4096
14 |
--------------------------------------------------------------------------------
/.github/workflows/fly-deploy.yml:
--------------------------------------------------------------------------------
1 | # See https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/
2 |
3 | name: Fly Deploy
4 | on:
5 | push:
6 | branches:
7 | - main
8 | jobs:
9 | deploy:
10 | name: Deploy app
11 | runs-on: ubuntu-latest
12 | concurrency: deploy-group # optional: ensure only one action runs at a time
13 | steps:
14 | - uses: actions/checkout@v4
15 | - uses: superfly/flyctl-actions/setup-flyctl@master
16 | - run: flyctl deploy --remote-only
17 | env:
18 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
19 |
--------------------------------------------------------------------------------
/config/prod.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | # Note we also include the path to a cache manifest
4 | # containing the digested version of static files. This
5 | # manifest is generated by the `mix assets.deploy` task,
6 | # which you should run after static files are built and
7 | # before starting your production server.
8 | config :glitchtv, GlitchtvWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json"
9 |
10 | # Do not print debug messages in production
11 | config :logger, level: :info
12 |
13 | # Runtime production configuration, including reading
14 | # of environment variables, is done on config/runtime.exs.
15 |
--------------------------------------------------------------------------------
/lib/glitchtv_web/components/layouts/root.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
33 | //
34 | plugin(({ addVariant }) =>
35 | addVariant("phx-click-loading", [
36 | ".phx-click-loading&",
37 | ".phx-click-loading &",
38 | ])
39 | ),
40 | plugin(({ addVariant }) =>
41 | addVariant("phx-submit-loading", [
42 | ".phx-submit-loading&",
43 | ".phx-submit-loading &",
44 | ])
45 | ),
46 | plugin(({ addVariant }) =>
47 | addVariant("phx-change-loading", [
48 | ".phx-change-loading&",
49 | ".phx-change-loading &",
50 | ])
51 | ),
52 |
53 | // Embeds Heroicons (https://heroicons.com) into your app.css bundle
54 | // See your `CoreComponents.icon/1` for more information.
55 | //
56 | plugin(function ({ matchComponents, theme }) {
57 | let iconsDir = path.join(__dirname, "../deps/heroicons/optimized");
58 | let values = {};
59 | let icons = [
60 | ["", "/24/outline"],
61 | ["-solid", "/24/solid"],
62 | ["-mini", "/20/solid"],
63 | ["-micro", "/16/solid"],
64 | ];
65 | icons.forEach(([suffix, dir]) => {
66 | fs.readdirSync(path.join(iconsDir, dir)).forEach((file) => {
67 | let name = path.basename(file, ".svg") + suffix;
68 | values[name] = { name, fullPath: path.join(iconsDir, dir, file) };
69 | });
70 | });
71 | matchComponents(
72 | {
73 | hero: ({ name, fullPath }) => {
74 | let content = fs
75 | .readFileSync(fullPath)
76 | .toString()
77 | .replace(/\r?\n|\r/g, "");
78 | let size = theme("spacing.6");
79 | if (name.endsWith("-mini")) {
80 | size = theme("spacing.5");
81 | } else if (name.endsWith("-micro")) {
82 | size = theme("spacing.4");
83 | }
84 | return {
85 | [`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`,
86 | "-webkit-mask": `var(--hero-${name})`,
87 | mask: `var(--hero-${name})`,
88 | "mask-repeat": "no-repeat",
89 | "background-color": "currentColor",
90 | "vertical-align": "middle",
91 | display: "inline-block",
92 | width: size,
93 | height: size,
94 | };
95 | },
96 | },
97 | { values }
98 | );
99 | }),
100 | ],
101 | };
102 |
--------------------------------------------------------------------------------
/test/glitchtv/recordings_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Glitchtv.RecordingsTest do
2 | use Glitchtv.DataCase
3 |
4 | alias Glitchtv.Recordings
5 |
6 | describe "recordings" do
7 | alias Glitchtv.Recordings.Recording
8 |
9 | import Glitchtv.RecordingsFixtures
10 |
11 | @invalid_attrs %{
12 | date: nil,
13 | link: nil,
14 | description: nil,
15 | title: nil,
16 | thumbnail_link: nil,
17 | length_seconds: nil,
18 | views_count: nil
19 | }
20 |
21 | test "list_recordings/0 returns all recordings" do
22 | recording = recording_fixture()
23 | assert Recordings.list_recordings() == [recording]
24 | end
25 |
26 | test "get_recording!/1 returns the recording with given id" do
27 | recording = recording_fixture()
28 | assert Recordings.get_recording!(recording.id) == recording
29 | end
30 |
31 | test "create_recording/1 with valid data creates a recording" do
32 | valid_attrs = %{
33 | date: ~U[2025-02-11 12:28:00Z],
34 | link: "some link",
35 | description: "some description",
36 | title: "some title",
37 | thumbnail_link: "some thumbnail_link",
38 | length_seconds: 42,
39 | views_count: 42
40 | }
41 |
42 | assert {:ok, %Recording{} = recording} = Recordings.create_recording(valid_attrs)
43 | assert recording.date == ~U[2025-02-11 12:28:00Z]
44 | assert recording.link == "some link"
45 | assert recording.description == "some description"
46 | assert recording.title == "some title"
47 | assert recording.thumbnail_link == "some thumbnail_link"
48 | assert recording.length_seconds == 42
49 | assert recording.views_count == 42
50 | end
51 |
52 | test "create_recording/1 with invalid data returns error changeset" do
53 | assert {:error, %Ecto.Changeset{}} = Recordings.create_recording(@invalid_attrs)
54 | end
55 |
56 | test "update_recording/2 with valid data updates the recording" do
57 | recording = recording_fixture()
58 |
59 | update_attrs = %{
60 | date: ~U[2025-02-12 12:28:00Z],
61 | link: "some updated link",
62 | description: "some updated description",
63 | title: "some updated title",
64 | thumbnail_link: "some updated thumbnail_link",
65 | length_seconds: 43,
66 | views_count: 43
67 | }
68 |
69 | assert {:ok, %Recording{} = recording} =
70 | Recordings.update_recording(recording, update_attrs)
71 |
72 | assert recording.date == ~U[2025-02-12 12:28:00Z]
73 | assert recording.link == "some updated link"
74 | assert recording.description == "some updated description"
75 | assert recording.title == "some updated title"
76 | assert recording.thumbnail_link == "some updated thumbnail_link"
77 | assert recording.length_seconds == 43
78 | assert recording.views_count == 43
79 | end
80 |
81 | test "update_recording/2 with invalid data returns error changeset" do
82 | recording = recording_fixture()
83 | assert {:error, %Ecto.Changeset{}} = Recordings.update_recording(recording, @invalid_attrs)
84 | assert recording == Recordings.get_recording!(recording.id)
85 | end
86 |
87 | test "delete_recording/1 deletes the recording" do
88 | recording = recording_fixture()
89 | assert {:ok, %Recording{}} = Recordings.delete_recording(recording)
90 | assert_raise Ecto.NoResultsError, fn -> Recordings.get_recording!(recording.id) end
91 | end
92 |
93 | test "change_recording/1 returns a recording changeset" do
94 | recording = recording_fixture()
95 | assert %Ecto.Changeset{} = Recordings.change_recording(recording)
96 | end
97 | end
98 | end
99 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Find eligible builder and runner images on Docker Hub. We use Ubuntu/Debian
2 | # instead of Alpine to avoid DNS resolution issues in production.
3 | #
4 | # https://hub.docker.com/r/hexpm/elixir/tags?page=1&name=ubuntu
5 | # https://hub.docker.com/_/ubuntu?tab=tags
6 | #
7 | # This file is based on these images:
8 | #
9 | # - https://hub.docker.com/r/hexpm/elixir/tags - for the build image
10 | # - https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye-20250203-slim - for the release image
11 | # - https://pkgs.org/ - resource for finding needed packages
12 | # - Ex: hexpm/elixir:1.18.2-erlang-27.2.1-debian-bullseye-20250203-slim
13 | #
14 | ARG ELIXIR_VERSION=1.17.2
15 | ARG OTP_VERSION=27.0.1
16 | ARG DEBIAN_VERSION=bookworm-20240701-slim
17 |
18 |
19 | ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"
20 | ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}"
21 |
22 | FROM ${BUILDER_IMAGE} as builder
23 |
24 | # install build dependencies
25 | RUN apt-get update -y && apt-get install -y build-essential wget git pkg-config libssl-dev \
26 | && apt-get clean && rm -f /var/lib/apt/lists/*_*
27 |
28 | # Install litestream
29 | ARG LITESTREAM_VERSION=0.3.13
30 | RUN wget https://github.com/benbjohnson/litestream/releases/download/v${LITESTREAM_VERSION}/litestream-v${LITESTREAM_VERSION}-linux-amd64.deb \
31 | && dpkg -i litestream-v${LITESTREAM_VERSION}-linux-amd64.deb
32 |
33 | # prepare build dir
34 | WORKDIR /app
35 |
36 | # install hex + rebar
37 | RUN mix local.hex --force && \
38 | mix local.rebar --force
39 |
40 | # set build ENV
41 | ENV MIX_ENV="prod"
42 |
43 | # install mix dependencies
44 | COPY mix.exs mix.lock ./
45 | RUN mix deps.get --only $MIX_ENV
46 | RUN mkdir config
47 |
48 | # copy compile-time config files before we compile dependencies
49 | # to ensure any relevant config change will trigger the dependencies
50 | # to be re-compiled.
51 | COPY config/config.exs config/${MIX_ENV}.exs config/
52 | RUN mix deps.compile
53 |
54 | COPY priv priv
55 |
56 | COPY lib lib
57 |
58 | COPY assets assets
59 |
60 | # compile assets
61 | RUN mix assets.deploy
62 |
63 | # Compile the release
64 | RUN mix compile
65 |
66 | # Changes to config/runtime.exs don't require recompiling the code
67 | COPY config/runtime.exs config/
68 |
69 | COPY rel rel
70 | RUN mix release
71 |
72 | # start a new build stage so that the final image will only contain
73 | # the compiled release and other runtime necessities
74 | FROM ${RUNNER_IMAGE}
75 |
76 | RUN apt-get update -y && \
77 | apt-get install -y libstdc++6 openssl libncurses5 locales ca-certificates ffmpeg\
78 | && apt-get clean && rm -f /var/lib/apt/lists/*_*
79 |
80 | # Set the locale
81 | RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
82 |
83 | ENV LANG en_US.UTF-8
84 | ENV LANGUAGE en_US:en
85 | ENV LC_ALL en_US.UTF-8
86 |
87 | WORKDIR "/app"
88 | RUN chown nobody /app
89 |
90 | # set runner ENV
91 | ENV MIX_ENV="prod"
92 |
93 | # Only copy the final release from the build stage
94 | COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/glitchtv ./
95 |
96 | # Copy Litestream binary from build stage
97 | COPY --from=builder /usr/bin/litestream /usr/bin/litestream
98 | COPY litestream.sh /app/bin/litestream.sh
99 | COPY config/litestream.yml /etc/litestream.yml
100 |
101 | USER nobody
102 |
103 | # If using an environment that doesn't automatically reap zombie processes, it is
104 | # advised to add an init process such as tini via `apt-get install`
105 | # above and adding an entrypoint. See https://github.com/krallin/tini for details
106 | # ENTRYPOINT ["/tini", "--"]
107 |
108 | # Run litestream script as entrypoint
109 | ENTRYPOINT ["/bin/bash", "/app/bin/litestream.sh"]
110 |
111 | CMD ["/app/bin/server"]
112 |
--------------------------------------------------------------------------------
/config/runtime.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | # config/runtime.exs is executed for all environments, including
4 | # during releases. It is executed after compilation and before the
5 | # system starts, so it is typically used to load production configuration
6 | # and secrets from environment variables or elsewhere. Do not define
7 | # any compile-time configuration in here, as it won't be applied.
8 | # The block below contains prod specific runtime configuration.
9 |
10 | # ## Using releases
11 | #
12 | # If you use `mix release`, you need to explicitly enable the server
13 | # by passing the PHX_SERVER=true when you start it:
14 | #
15 | # PHX_SERVER=true bin/glitchtv start
16 | #
17 | # Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
18 | # script that automatically sets the env var above.
19 | if System.get_env("PHX_SERVER") do
20 | config :glitchtv, GlitchtvWeb.Endpoint, server: true
21 | end
22 |
23 | if System.get_env("FLY_APP_NAME") do
24 | config :glitchtv, ice_ip_filter: &ExWebRTC.ICE.FlyIpFilter.ip_filter/1
25 | end
26 |
27 | config :glitchtv, bucket_name: System.get_env("BUCKET_NAME")
28 |
29 | if config_env() == :prod do
30 | database_path =
31 | System.get_env("DATABASE_PATH") ||
32 | raise """
33 | environment variable DATABASE_PATH is missing.
34 | For example: /etc/glitchtv/glitchtv.db
35 | """
36 |
37 | config :glitchtv, Glitchtv.Repo,
38 | database: database_path,
39 | pool_size: String.to_integer(System.get_env("POOL_SIZE") || "5")
40 |
41 | # The secret key base is used to sign/encrypt cookies and other secrets.
42 | # A default value is used in config/dev.exs and config/test.exs but you
43 | # want to use a different value for prod and you most likely don't want
44 | # to check this value into version control, so we use an environment
45 | # variable instead.
46 | secret_key_base =
47 | System.get_env("SECRET_KEY_BASE") ||
48 | raise """
49 | environment variable SECRET_KEY_BASE is missing.
50 | You can generate one by calling: mix phx.gen.secret
51 | """
52 |
53 | host = System.get_env("PHX_HOST") || "example.com"
54 | port = String.to_integer(System.get_env("PORT") || "4000")
55 |
56 | config :glitchtv, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
57 |
58 | config :glitchtv, GlitchtvWeb.Endpoint,
59 | url: [host: host, port: 443, scheme: "https"],
60 | http: [
61 | # Enable IPv6 and bind on all interfaces.
62 | # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
63 | # See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0
64 | # for details about using IPv6 vs IPv4 and loopback vs public addresses.
65 | ip: {0, 0, 0, 0, 0, 0, 0, 0},
66 | port: port
67 | ],
68 | secret_key_base: secret_key_base
69 |
70 | # ## SSL Support
71 | #
72 | # To get SSL working, you will need to add the `https` key
73 | # to your endpoint configuration:
74 | #
75 | # config :glitchtv, GlitchtvWeb.Endpoint,
76 | # https: [
77 | # ...,
78 | # port: 443,
79 | # cipher_suite: :strong,
80 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
81 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
82 | # ]
83 | #
84 | # The `cipher_suite` is set to `:strong` to support only the
85 | # latest and more secure SSL ciphers. This means old browsers
86 | # and clients may not be supported. You can set it to
87 | # `:compatible` for wider support.
88 | #
89 | # `:keyfile` and `:certfile` expect an absolute path to the key
90 | # and cert in disk or a relative path inside priv, for example
91 | # "priv/ssl/server.key". For all supported SSL configuration
92 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
93 | #
94 | # We also recommend setting `force_ssl` in your config/prod.exs,
95 | # ensuring no data is ever sent via http, always redirecting to https:
96 | #
97 | # config :glitchtv, GlitchtvWeb.Endpoint,
98 | # force_ssl: [hsts: true]
99 | #
100 | # Check `Plug.SSL` for all available options in `force_ssl`.
101 | end
102 |
--------------------------------------------------------------------------------
/test/glitchtv_web/live/recording_live_test.exs:
--------------------------------------------------------------------------------
1 | defmodule GlitchtvWeb.RecordingLiveTest do
2 | use GlitchtvWeb.ConnCase
3 |
4 | import Phoenix.LiveViewTest
5 | import Glitchtv.RecordingsFixtures
6 |
7 | @create_attrs %{
8 | date: "2025-02-11T12:28:00Z",
9 | link: "some link",
10 | description: "some description",
11 | title: "some title",
12 | thumbnail_link: "some thumbnail_link",
13 | length_seconds: 42,
14 | views_count: 42
15 | }
16 | @update_attrs %{
17 | date: "2025-02-12T12:28:00Z",
18 | link: "some updated link",
19 | description: "some updated description",
20 | title: "some updated title",
21 | thumbnail_link: "some updated thumbnail_link",
22 | length_seconds: 43,
23 | views_count: 43
24 | }
25 | @invalid_attrs %{
26 | date: nil,
27 | link: nil,
28 | description: nil,
29 | title: nil,
30 | thumbnail_link: nil,
31 | length_seconds: nil,
32 | views_count: nil
33 | }
34 |
35 | defp create_recording(_) do
36 | recording = recording_fixture()
37 | %{recording: recording}
38 | end
39 |
40 | describe "Index" do
41 | setup [:create_recording]
42 |
43 | test "lists all recordings", %{conn: conn, recording: recording} do
44 | {:ok, _index_live, html} = live(conn, ~p"/recordings")
45 |
46 | assert html =~ "Listing Recordings"
47 | assert html =~ recording.link
48 | end
49 |
50 | test "saves new recording", %{conn: conn} do
51 | {:ok, index_live, _html} = live(conn, ~p"/recordings")
52 |
53 | assert index_live |> element("a", "New Recording") |> render_click() =~
54 | "New Recording"
55 |
56 | assert_patch(index_live, ~p"/recordings/new")
57 |
58 | assert index_live
59 | |> form("#recording-form", recording: @invalid_attrs)
60 | |> render_change() =~ "can't be blank"
61 |
62 | assert index_live
63 | |> form("#recording-form", recording: @create_attrs)
64 | |> render_submit()
65 |
66 | assert_patch(index_live, ~p"/recordings")
67 |
68 | html = render(index_live)
69 | assert html =~ "Recording created successfully"
70 | assert html =~ "some link"
71 | end
72 |
73 | test "updates recording in listing", %{conn: conn, recording: recording} do
74 | {:ok, index_live, _html} = live(conn, ~p"/recordings")
75 |
76 | assert index_live |> element("#recordings-#{recording.id} a", "Edit") |> render_click() =~
77 | "Edit Recording"
78 |
79 | assert_patch(index_live, ~p"/recordings/#{recording}/edit")
80 |
81 | assert index_live
82 | |> form("#recording-form", recording: @invalid_attrs)
83 | |> render_change() =~ "can't be blank"
84 |
85 | assert index_live
86 | |> form("#recording-form", recording: @update_attrs)
87 | |> render_submit()
88 |
89 | assert_patch(index_live, ~p"/recordings")
90 |
91 | html = render(index_live)
92 | assert html =~ "Recording updated successfully"
93 | assert html =~ "some updated link"
94 | end
95 |
96 | test "deletes recording in listing", %{conn: conn, recording: recording} do
97 | {:ok, index_live, _html} = live(conn, ~p"/recordings")
98 |
99 | assert index_live |> element("#recordings-#{recording.id} a", "Delete") |> render_click()
100 | refute has_element?(index_live, "#recordings-#{recording.id}")
101 | end
102 | end
103 |
104 | describe "Show" do
105 | setup [:create_recording]
106 |
107 | test "displays recording", %{conn: conn, recording: recording} do
108 | {:ok, _show_live, html} = live(conn, ~p"/recordings/#{recording}")
109 |
110 | assert html =~ "Show Recording"
111 | assert html =~ recording.link
112 | end
113 |
114 | test "updates recording within modal", %{conn: conn, recording: recording} do
115 | {:ok, show_live, _html} = live(conn, ~p"/recordings/#{recording}")
116 |
117 | assert show_live |> element("a", "Edit") |> render_click() =~
118 | "Edit Recording"
119 |
120 | assert_patch(show_live, ~p"/recordings/#{recording}/show/edit")
121 |
122 | assert show_live
123 | |> form("#recording-form", recording: @invalid_attrs)
124 | |> render_change() =~ "can't be blank"
125 |
126 | assert show_live
127 | |> form("#recording-form", recording: @update_attrs)
128 | |> render_submit()
129 |
130 | assert_patch(show_live, ~p"/recordings/#{recording}")
131 |
132 | html = render(show_live)
133 | assert html =~ "Recording updated successfully"
134 | assert html =~ "some updated link"
135 | end
136 | end
137 | end
138 |
--------------------------------------------------------------------------------
/lib/glitchtv_web/live/stream_viewer_live.ex:
--------------------------------------------------------------------------------
1 | defmodule GlitchtvWeb.StreamViewerLive do
2 | use GlitchtvWeb, :live_view
3 |
4 | alias LiveExWebRTC.Player
5 | alias Phoenix.Presence
6 | alias Phoenix.Socket.Broadcast
7 | alias GlitchtvWeb.ChatLive
8 | alias GlitchtvWeb.Presence
9 |
10 | @impl true
11 | def render(assigns) do
12 | ~H"""
13 |
14 |
15 |
16 |
19 |
20 |
21 |
22 |
23 | <%= if @stream_metadata.streaming? do %>
24 | <.live_dropping />
25 | <% end %>
26 |
27 | {if @stream_metadata, do: @stream_metadata.title, else: "The stream is offline"}
28 |
29 |
30 |
31 | <.dropping>
32 | <%= if @stream_metadata.streaming? do %>
33 | Started:
34 |
35 | {@stream_duration} minutes ago
36 |
37 | <% else %>
38 | Stream is offline
39 | <% end %>
40 |
41 | <.dropping>
42 |
43 | {@viewers_count} viewers
44 |
45 |
46 | <.share_button />
47 |
48 |
49 | {@stream_metadata.description}
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | """
58 | end
59 |
60 | defp live_dropping(assigns) do
61 | ~H"""
62 |
63 | live
64 |
65 | """
66 | end
67 |
68 | @impl true
69 | def mount(_params, _session, socket) do
70 | if connected?(socket) do
71 | Phoenix.PubSub.subscribe(Glitchtv.PubSub, "stream_info:status")
72 | Phoenix.PubSub.subscribe(Glitchtv.PubSub, "stream_info:viewers")
73 | {:ok, _ref} = Presence.track(self(), "stream_info:viewers", inspect(self()), %{})
74 | end
75 |
76 | metadata = Glitchtv.StreamService.get_stream_metadata()
77 |
78 | socket =
79 | Player.attach(socket,
80 | id: "player",
81 | publisher_id: "publisher",
82 | pubsub: Glitchtv.PubSub,
83 | ice_servers: [%{urls: "stun:stun.l.google.com:19302"}]
84 | # ice_ip_filter: Application.get_env(:live_broadcaster, :ice_ip_filter)
85 | )
86 | |> assign(:page_title, "Stream")
87 | |> assign(:stream_metadata, metadata)
88 | |> assign(:viewers_count, get_viewers_count())
89 | |> assign(:stream_duration, measure_duration(metadata.started))
90 |
91 | {:ok, socket}
92 | end
93 |
94 | @impl Phoenix.LiveView
95 | def handle_info({:started, started}, socket) do
96 | metadata = %{socket.assigns.stream_metadata | streaming?: true, started: started}
97 | {:noreply, assign(socket, :stream_metadata, metadata)}
98 | end
99 |
100 | def handle_info({:changed, {title, description}}, socket) do
101 | metadata = %{socket.assigns.stream_metadata | title: title, description: description}
102 | {:noreply, assign(socket, :stream_metadata, metadata)}
103 | end
104 |
105 | def handle_info(:finished, socket) do
106 | metadata = %{socket.assigns.stream_metadata | streaming?: false, started: nil}
107 | {:noreply, assign(socket, :stream_metadata, metadata)}
108 | end
109 |
110 | def handle_info(:tick, socket) do
111 | socket =
112 | socket
113 | |> assign(
114 | :stream_duration,
115 | measure_duration(socket.assigns.stream_metadata.started)
116 | )
117 |
118 | {:noreply, socket}
119 | end
120 |
121 | def handle_info(%Broadcast{event: "presence_diff"}, socket) do
122 | {:noreply, assign(socket, :viewers_count, get_viewers_count())}
123 | end
124 |
125 | def get_viewers_count() do
126 | map_size(Presence.list("stream_info:viewers"))
127 | end
128 |
129 | defp measure_duration(started_timestamp) do
130 | case started_timestamp do
131 | nil ->
132 | 0
133 |
134 | t ->
135 | DateTime.utc_now()
136 | |> DateTime.diff(t, :minute)
137 | end
138 | end
139 | end
140 |
--------------------------------------------------------------------------------
/priv/static/images/elixir-webrtc-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/priv/static/images/elixir-webrtc-dark-mode-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/lib/glitchtv_web/live/chat_live.ex:
--------------------------------------------------------------------------------
1 | defmodule GlitchtvWeb.ChatLive do
2 | use Phoenix.LiveView
3 |
4 | attr(:socket, Phoenix.LiveView.Socket, required: true, doc: "Parent live view socket")
5 | attr(:id, :string, required: true, doc: "Component id")
6 |
7 | def live_render(assigns) do
8 | ~H"""
9 | {live_render(@socket, __MODULE__, id: @id)}
10 | """
11 | end
12 |
13 | @impl true
14 | def render(assigns) do
15 | ~H"""
16 |
97 | """
98 | end
99 |
100 | @impl true
101 | def mount(_params, _session, socket) do
102 | if connected?(socket) do
103 | subscribe()
104 | end
105 |
106 | socket =
107 | socket
108 | |> stream(:messages, [])
109 | |> assign(msg_body: nil, author: nil, next_msg_id: 0)
110 | |> assign(max_msg_length: 500, max_nickname_length: 25)
111 | |> assign(joined: false)
112 |
113 | {:ok, socket}
114 | end
115 |
116 | @impl true
117 | def handle_info({:new_msg, msg}, socket) do
118 | {:noreply, stream(socket, :messages, [msg])}
119 | end
120 |
121 | @impl true
122 | def handle_event("validate-form", %{"author" => author}, socket) do
123 | {:noreply, assign(socket, author: author)}
124 | end
125 |
126 | def handle_event("validate-form", %{"body" => body}, socket) do
127 | {:noreply, assign(socket, msg_body: body)}
128 | end
129 |
130 | def handle_event("submit-form", %{"body" => body}, socket) do
131 | if body != "" do
132 | id = socket.assigns.next_msg_id
133 | send_message(body, socket.assigns.author, id)
134 | {:noreply, assign(socket, msg_body: nil, next_msg_id: id + 1)}
135 | else
136 | {:noreply, socket}
137 | end
138 | end
139 |
140 | def handle_event("submit-form", %{"author" => _}, socket) do
141 | {:noreply, assign(socket, joined: true)}
142 | end
143 |
144 | defp subscribe() do
145 | Phoenix.PubSub.subscribe(Glitchtv.PubSub, "chatroom")
146 | end
147 |
148 | defp send_message(body, author, id) do
149 | {:ok, timestamp} = DateTime.now("Etc/UTC")
150 | msg = %{author: author, body: body, id: "#{author}:#{id}", timestamp: timestamp}
151 | Phoenix.PubSub.broadcast(Glitchtv.PubSub, "chatroom", {:new_msg, msg})
152 | end
153 | end
154 |
--------------------------------------------------------------------------------
/lib/glitchtv_web/live/streamer_live.ex:
--------------------------------------------------------------------------------
1 | defmodule GlitchtvWeb.StreamerLive do
2 | use GlitchtvWeb, :live_view
3 |
4 | alias LiveExWebRTC.Publisher
5 | alias Phoenix.Socket.Broadcast
6 | alias GlitchtvWeb.ChatLive
7 | alias GlitchtvWeb.StreamViewerLive
8 |
9 | # XXX add this as defaults in live_ex_webrtc, so that recordings work by default?
10 | @video_codecs [
11 | %ExWebRTC.RTPCodecParameters{
12 | payload_type: 96,
13 | mime_type: "video/VP8",
14 | clock_rate: 90_000
15 | }
16 | ]
17 |
18 | @audio_codecs [
19 | %ExWebRTC.RTPCodecParameters{
20 | payload_type: 111,
21 | mime_type: "audio/opus",
22 | clock_rate: 48_000,
23 | channels: 2
24 | }
25 | ]
26 |
27 | @impl true
28 | def render(assigns) do
29 | ~H"""
30 |
31 |
32 |
33 |
34 |
Stream details
35 | <.dropping class="py-1">
36 |
37 | <.icon name="hero-eye" class="w-4 h-4" />
38 | {@viewers_count}
39 |
40 |
41 |
42 |
43 |
44 |
52 |
53 | Save
54 |
55 |
56 | {@form_data.description}
62 |
63 |
64 |
67 |
68 |
69 |
70 | """
71 | end
72 |
73 | @impl true
74 | def mount(_params, _session, socket) do
75 | if connected?(socket) do
76 | Phoenix.PubSub.subscribe(Glitchtv.PubSub, "stream_info:viewers")
77 | end
78 |
79 | socket =
80 | Publisher.attach(socket,
81 | id: "publisher",
82 | pubsub: Glitchtv.PubSub,
83 | on_connected: &on_connected/1,
84 | on_disconnected: &on_disconnected/1,
85 | on_recording_finished: &on_recording_finished/2,
86 | on_recorder_message: &on_recorder_message/2,
87 | ice_ip_filter: Application.get_env(:glitchtv, :ice_ip_filter, fn _ -> true end),
88 | ice_servers: [%{urls: "stun:stun.l.google.com:19302"}],
89 | recorder_opts: [
90 | s3_upload_config: [bucket_name: Application.get_env(:glitchtv, :bucket_name)]
91 | ],
92 | video_codecs: @video_codecs,
93 | audio_codecs: @audio_codecs
94 | )
95 | |> assign(:form_data, %{title: "", description: ""})
96 | |> assign(:page_title, "Streamer Panel")
97 | |> assign(:viewers_count, StreamViewerLive.get_viewers_count())
98 |
99 | Glitchtv.StreamService.put_stream_metadata(%{title: "", description: ""})
100 | {:ok, socket}
101 | end
102 |
103 | @impl true
104 | def handle_event(
105 | "stream-config-update",
106 | %{"title" => title, "description" => description},
107 | socket
108 | ) do
109 | Glitchtv.StreamService.put_stream_metadata(%{title: title, description: description})
110 |
111 | {:noreply, socket}
112 | end
113 |
114 | def handle_event(
115 | "update-title",
116 | %{"title" => title},
117 | socket
118 | ) do
119 | socket =
120 | socket
121 | |> assign(:form_data, %{socket.assigns.form_data | title: title})
122 |
123 | {:noreply, socket}
124 | end
125 |
126 | def handle_event(
127 | "update-description",
128 | %{"description" => description},
129 | socket
130 | ) do
131 | socket =
132 | socket
133 | |> assign(:form_data, %{socket.assigns.form_data | description: description})
134 |
135 | {:noreply, socket}
136 | end
137 |
138 | defp on_connected("publisher") do
139 | Glitchtv.StreamService.stream_started()
140 | end
141 |
142 | defp on_disconnected("publisher") do
143 | Glitchtv.StreamService.stream_ended()
144 | end
145 |
146 | # Gets called before on_disconnected, so everything is OK
147 | defp on_recording_finished("publisher", {:ok, _manifest, ref}) do
148 | metadata = Glitchtv.StreamService.get_stream_metadata()
149 |
150 | if ref != nil do
151 | Glitchtv.RecordingsService.upload_started(ref, metadata)
152 | end
153 | end
154 |
155 | defp on_recorder_message(
156 | "publisher",
157 | {:ex_webrtc_recorder, _, {:upload_complete, ref, manifest}}
158 | ) do
159 | Glitchtv.RecordingsService.upload_complete(ref, manifest)
160 | end
161 |
162 | @impl Phoenix.LiveView
163 | def handle_info(%Broadcast{event: "presence_diff"}, socket) do
164 | {:noreply, assign(socket, :viewers_count, StreamViewerLive.get_viewers_count())}
165 | end
166 | end
167 |
--------------------------------------------------------------------------------
/assets/vendor/topbar.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license MIT
3 | * topbar 2.0.0, 2023-02-04
4 | * https://buunguyen.github.io/topbar
5 | * Copyright (c) 2021 Buu Nguyen
6 | */
7 | (function (window, document) {
8 | "use strict";
9 |
10 | // https://gist.github.com/paulirish/1579671
11 | (function () {
12 | var lastTime = 0;
13 | var vendors = ["ms", "moz", "webkit", "o"];
14 | for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
15 | window.requestAnimationFrame =
16 | window[vendors[x] + "RequestAnimationFrame"];
17 | window.cancelAnimationFrame =
18 | window[vendors[x] + "CancelAnimationFrame"] ||
19 | window[vendors[x] + "CancelRequestAnimationFrame"];
20 | }
21 | if (!window.requestAnimationFrame)
22 | window.requestAnimationFrame = function (callback, element) {
23 | var currTime = new Date().getTime();
24 | var timeToCall = Math.max(0, 16 - (currTime - lastTime));
25 | var id = window.setTimeout(function () {
26 | callback(currTime + timeToCall);
27 | }, timeToCall);
28 | lastTime = currTime + timeToCall;
29 | return id;
30 | };
31 | if (!window.cancelAnimationFrame)
32 | window.cancelAnimationFrame = function (id) {
33 | clearTimeout(id);
34 | };
35 | })();
36 |
37 | var canvas,
38 | currentProgress,
39 | showing,
40 | progressTimerId = null,
41 | fadeTimerId = null,
42 | delayTimerId = null,
43 | addEvent = function (elem, type, handler) {
44 | if (elem.addEventListener) elem.addEventListener(type, handler, false);
45 | else if (elem.attachEvent) elem.attachEvent("on" + type, handler);
46 | else elem["on" + type] = handler;
47 | },
48 | options = {
49 | autoRun: true,
50 | barThickness: 3,
51 | barColors: {
52 | 0: "rgba(26, 188, 156, .9)",
53 | ".25": "rgba(52, 152, 219, .9)",
54 | ".50": "rgba(241, 196, 15, .9)",
55 | ".75": "rgba(230, 126, 34, .9)",
56 | "1.0": "rgba(211, 84, 0, .9)",
57 | },
58 | shadowBlur: 10,
59 | shadowColor: "rgba(0, 0, 0, .6)",
60 | className: null,
61 | },
62 | repaint = function () {
63 | canvas.width = window.innerWidth;
64 | canvas.height = options.barThickness * 5; // need space for shadow
65 |
66 | var ctx = canvas.getContext("2d");
67 | ctx.shadowBlur = options.shadowBlur;
68 | ctx.shadowColor = options.shadowColor;
69 |
70 | var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
71 | for (var stop in options.barColors)
72 | lineGradient.addColorStop(stop, options.barColors[stop]);
73 | ctx.lineWidth = options.barThickness;
74 | ctx.beginPath();
75 | ctx.moveTo(0, options.barThickness / 2);
76 | ctx.lineTo(
77 | Math.ceil(currentProgress * canvas.width),
78 | options.barThickness / 2
79 | );
80 | ctx.strokeStyle = lineGradient;
81 | ctx.stroke();
82 | },
83 | createCanvas = function () {
84 | canvas = document.createElement("canvas");
85 | var style = canvas.style;
86 | style.position = "fixed";
87 | style.top = style.left = style.right = style.margin = style.padding = 0;
88 | style.zIndex = 100001;
89 | style.display = "none";
90 | if (options.className) canvas.classList.add(options.className);
91 | document.body.appendChild(canvas);
92 | addEvent(window, "resize", repaint);
93 | },
94 | topbar = {
95 | config: function (opts) {
96 | for (var key in opts)
97 | if (options.hasOwnProperty(key)) options[key] = opts[key];
98 | },
99 | show: function (delay) {
100 | if (showing) return;
101 | if (delay) {
102 | if (delayTimerId) return;
103 | delayTimerId = setTimeout(() => topbar.show(), delay);
104 | } else {
105 | showing = true;
106 | if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId);
107 | if (!canvas) createCanvas();
108 | canvas.style.opacity = 1;
109 | canvas.style.display = "block";
110 | topbar.progress(0);
111 | if (options.autoRun) {
112 | (function loop() {
113 | progressTimerId = window.requestAnimationFrame(loop);
114 | topbar.progress(
115 | "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2)
116 | );
117 | })();
118 | }
119 | }
120 | },
121 | progress: function (to) {
122 | if (typeof to === "undefined") return currentProgress;
123 | if (typeof to === "string") {
124 | to =
125 | (to.indexOf("+") >= 0 || to.indexOf("-") >= 0
126 | ? currentProgress
127 | : 0) + parseFloat(to);
128 | }
129 | currentProgress = to > 1 ? 1 : to;
130 | repaint();
131 | return currentProgress;
132 | },
133 | hide: function () {
134 | clearTimeout(delayTimerId);
135 | delayTimerId = null;
136 | if (!showing) return;
137 | showing = false;
138 | if (progressTimerId != null) {
139 | window.cancelAnimationFrame(progressTimerId);
140 | progressTimerId = null;
141 | }
142 | (function loop() {
143 | if (topbar.progress("+.1") >= 1) {
144 | canvas.style.opacity -= 0.05;
145 | if (canvas.style.opacity <= 0.05) {
146 | canvas.style.display = "none";
147 | fadeTimerId = null;
148 | return;
149 | }
150 | }
151 | fadeTimerId = window.requestAnimationFrame(loop);
152 | })();
153 | },
154 | };
155 |
156 | if (typeof module === "object" && typeof module.exports === "object") {
157 | module.exports = topbar;
158 | } else if (typeof define === "function" && define.amd) {
159 | define(function () {
160 | return topbar;
161 | });
162 | } else {
163 | this.topbar = topbar;
164 | }
165 | }.call(this, window, document));
166 |
--------------------------------------------------------------------------------
/priv/static/images/swm-dark-mode-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/priv/static/images/swm-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/lib/glitchtv_web/controllers/page_html/home.html.heex:
--------------------------------------------------------------------------------
1 | <.flash_group flash={@flash} />
2 |
3 |
10 |
11 |
15 |
19 |
24 |
29 |
34 |
39 |
40 |
41 |
42 |
43 |
44 |
48 |
49 |
50 | Phoenix Framework
51 |
52 | v{Application.spec(:phoenix, :vsn)}
53 |
54 |
55 |
56 | Peace of mind from prototype to production.
57 |
58 |
59 | Build rich, interactive web applications quickly, with less code and fewer moving parts. Join our growing community of developers using Phoenix to craft APIs, HTML5 apps and more, for fun or at scale.
60 |
61 |
62 |
63 |
133 |
134 |
149 |
164 |
188 |
203 |
218 |
219 |
220 |
221 |
222 |
223 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "bandit": {:hex, :bandit, "1.6.11", "2fbadd60c95310eefb4ba7f1e58810aa8956e18c664a3b2029d57edb7d28d410", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "543f3f06b4721619a1220bed743aa77bf7ecc9c093ba9fab9229ff6b99eacc65"},
3 | "bunch": {:hex, :bunch, "1.6.1", "5393d827a64d5f846092703441ea50e65bc09f37fd8e320878f13e63d410aec7", [:mix], [], "hexpm", "286cc3add551628b30605efbe2fca4e38cc1bea89bcd0a1a7226920b3364fe4a"},
4 | "bunch_native": {:hex, :bunch_native, "0.5.0", "8ac1536789a597599c10b652e0b526d8833348c19e4739a0759a2bedfd924e63", [:mix], [{:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "24190c760e32b23b36edeb2dc4852515c7c5b3b8675b1a864e0715bdd1c8f80d"},
5 | "bundlex": {:hex, :bundlex, "1.5.4", "3726acd463f4d31894a59bbc177c17f3b574634a524212f13469f41c4834a1d9", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:elixir_uuid, "~> 1.2", [hex: :elixir_uuid, repo: "hexpm", optional: false]}, {:qex, "~> 0.5", [hex: :qex, repo: "hexpm", optional: false]}, {:req, ">= 0.4.0", [hex: :req, repo: "hexpm", optional: false]}, {:zarex, "~> 1.0", [hex: :zarex, repo: "hexpm", optional: false]}], "hexpm", "e745726606a560275182a8ac1c8ebd5e11a659bb7460d8abf30f397e59b4c5d2"},
6 | "castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"},
7 | "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"},
8 | "certifi": {:hex, :certifi, "2.14.0", "ed3bef654e69cde5e6c022df8070a579a79e8ba2368a00acf3d75b82d9aceeed", [:rebar3], [], "hexpm", "ea59d87ef89da429b8e905264fdec3419f84f2215bb3d81e07a18aac919026c3"},
9 | "crc": {:hex, :crc, "0.10.5", "ee12a7c056ac498ef2ea985ecdc9fa53c1bfb4e53a484d9f17ff94803707dfd8", [:mix, :rebar3], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3e673b6495a9525c5c641585af1accba59a1eb33de697bedf341e247012c2c7f"},
10 | "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
11 | "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
12 | "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"},
13 | "ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"},
14 | "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"},
15 | "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.19.0", "00030bbaba150369ff3754bbc0d2c28858e8f528ae406bf6997d1772d3a03203", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "297b16750fe229f3056fe32afd3247de308094e8b0298aef0d73a8493ce97c81"},
16 | "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
17 | "elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"},
18 | "esbuild": {:hex, :esbuild, "0.9.0", "f043eeaca4932ca8e16e5429aebd90f7766f31ac160a25cbd9befe84f2bc068f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b415027f71d5ab57ef2be844b2a10d0c1b5a492d431727f43937adce22ba45ae"},
19 | "ex_aws": {:hex, :ex_aws, "2.5.9", "8e2455172f0e5cbe2f56dd68de514f0dae6bb26d6b6e2f435a06434cf9dbb412", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cbdb6ffb0e6c6368de05ed8641fe1376298ba23354674428e5b153a541f23359"},
20 | "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.7", "e571424d2f345299753382f3a01b005c422b1a460a8bc3ed47659b3d3ef91e9e", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "858e51241e50181e29aa2bc128fef548873a3a9cd580471f57eda5b64dec937f"},
21 | "ex_dtls": {:hex, :ex_dtls, "0.16.0", "3ae38025ccc77f6db573e2e391602fa9bbc02253c137d8d2d59469a66cbe806b", [:mix], [{:bundlex, "~> 1.5.3", [hex: :bundlex, repo: "hexpm", optional: false]}, {:unifex, "~> 1.0", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "2a4e30d74c6ddf95cc5b796423293c06a0da295454c3823819808ff031b4b361"},
22 | "ex_ice": {:hex, :ex_ice, "0.12.0", "b52ec3ff878d5fb632ef9facc7657dfdf59e2ff9f23e634b0918e6ce1a05af48", [:mix], [{:elixir_uuid, "~> 1.0", [hex: :elixir_uuid, repo: "hexpm", optional: false]}, {:ex_stun, "~> 0.2.0", [hex: :ex_stun, repo: "hexpm", optional: false]}, {:ex_turn, "~> 0.2.0", [hex: :ex_turn, repo: "hexpm", optional: false]}], "hexpm", "a86024a5fbf9431082784be4bb3606d3cde9218fb325a9f208ccd6e0abfd0d73"},
23 | "ex_libsrtp": {:hex, :ex_libsrtp, "0.7.2", "211bd89c08026943ce71f3e2c0231795b99cee748808ed3ae7b97cd8d2450b6b", [:mix], [{:bunch, "~> 1.6", [hex: :bunch, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.3", [hex: :bundlex, repo: "hexpm", optional: false]}, {:membrane_precompiled_dependency_provider, "~> 0.1.0", [hex: :membrane_precompiled_dependency_provider, repo: "hexpm", optional: false]}, {:unifex, "~> 1.1", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "2e20645d0d739a4ecdcf8d4810a0c198120c8a2f617f2b75b2e2e704d59f492a"},
24 | "ex_rtcp": {:hex, :ex_rtcp, "0.4.0", "f9e515462a9581798ff6413583a25174cfd2101c94a2ebee871cca7639886f0a", [:mix], [], "hexpm", "28956602cf210d692fcdaf3f60ca49681634e1deb28ace41246aee61ee22dc3b"},
25 | "ex_rtp": {:hex, :ex_rtp, "0.4.0", "1f1b5c1440a904706011e3afbb41741f5da309ce251cb986690ce9fd82636658", [:mix], [], "hexpm", "0f72d80d5953a62057270040f0f1ee6f955c08eeae82ac659c038001d7d5a790"},
26 | "ex_sdp": {:hex, :ex_sdp, "1.1.1", "1a7b049491e5ec02dad9251c53d960835dc5631321ae978ec331831f3e4f6d5f", [:mix], [{:bunch, "~> 1.3", [hex: :bunch, repo: "hexpm", optional: false]}, {:elixir_uuid, "~> 1.2", [hex: :elixir_uuid, repo: "hexpm", optional: false]}], "hexpm", "1b13a72ac9c5c695b8824dbdffc671be8cbb4c0d1ccb4ff76a04a6826759f233"},
27 | "ex_stun": {:hex, :ex_stun, "0.2.0", "feb1fc7db0356406655b2a617805e6c712b93308c8ea2bf0ba1197b1f0866deb", [:mix], [], "hexpm", "1e01ba8290082ccbf37acaa5190d1f69b51edd6de2026a8d6d51368b29d115d0"},
28 | "ex_turn": {:hex, :ex_turn, "0.2.0", "4e1f9b089e9a5ee44928d12370cc9ea7a89b84b2f6256832de65271212eb80de", [:mix], [{:ex_stun, "~> 0.2.0", [hex: :ex_stun, repo: "hexpm", optional: false]}], "hexpm", "08e884f0af2c4a147e3f8cd4ffe33e3452a256389f0956e55a8c4d75bf0e74cd"},
29 | "ex_webrtc": {:hex, :ex_webrtc, "0.12.0", "ad31fa5759c51dbdcbebe213f745de676e882145584b2f0add7d3b8c3d8a8d48", [:mix], [{:crc, "~> 0.10", [hex: :crc, repo: "hexpm", optional: false]}, {:ex_dtls, "~> 0.16.0", [hex: :ex_dtls, repo: "hexpm", optional: false]}, {:ex_ice, "~> 0.12.0", [hex: :ex_ice, repo: "hexpm", optional: false]}, {:ex_libsrtp, "~> 0.7.1", [hex: :ex_libsrtp, repo: "hexpm", optional: false]}, {:ex_rtcp, "~> 0.4.0", [hex: :ex_rtcp, repo: "hexpm", optional: false]}, {:ex_rtp, "~> 0.4.0", [hex: :ex_rtp, repo: "hexpm", optional: false]}, {:ex_sctp, "0.1.2", [hex: :ex_sctp, repo: "hexpm", optional: true]}, {:ex_sdp, "~> 1.0", [hex: :ex_sdp, repo: "hexpm", optional: false]}], "hexpm", "bb59d8593d5a2c1c4a18f78bf8b13c54951ca0255b59dcfdbaacf3a38e5f48e0"},
30 | "ex_webrtc_recorder": {:hex, :ex_webrtc_recorder, "0.1.0", "d813fe542bb5144ac1d7f7226535dd4b0287a0730a322d4a34bc70886f60643e", [:mix], [{:ex_aws, "~> 2.5", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:ex_aws_s3, "~> 2.5", [hex: :ex_aws_s3, repo: "hexpm", optional: true]}, {:ex_webrtc, "~> 0.12.0", [hex: :ex_webrtc, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "3b6183b2da994f9b55ce4a33a9440f6274eea3457983484849933c6731ecbda3"},
31 | "exqlite": {:hex, :exqlite, "0.29.0", "e6f1de4bfe3ce6e4c4260b15fef830705fa36632218dc7eafa0a5aba3a5d6e04", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a75f8a069fcdad3e5f95dfaddccd13c2112ea3b742fdcc234b96410e9c1bde00"},
32 | "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
33 | "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},
34 | "floki": {:hex, :floki, "0.37.1", "d7aaee758c8a5b4a7495799a4260754fec5530d95b9c383c03b27359dea117cf", [:mix], [], "hexpm", "673d040cb594d31318d514590246b6dd587ed341d3b67e17c1c0eb8ce7ca6f04"},
35 | "hackney": {:hex, :hackney, "1.23.0", "55cc09077112bcb4a69e54be46ed9bc55537763a96cd4a80a221663a7eafd767", [:rebar3], [{:certifi, "~> 2.14.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "6cd1c04cd15c81e5a493f167b226a15f0938a84fc8f0736ebe4ddcab65c0b44e"},
36 | "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]},
37 | "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
38 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
39 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
40 | "live_ex_webrtc": {:git, "https://github.com/elixir-webrtc/live_ex_webrtc.git", "d167b988e75fad2d03077037fa73845053b8bf32", []},
41 | "membrane_precompiled_dependency_provider": {:hex, :membrane_precompiled_dependency_provider, "0.1.2", "8af73b7dc15ba55c9f5fbfc0453d4a8edfb007ade54b56c37d626be0d1189aba", [:mix], [{:bundlex, "~> 1.4", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "7fe3e07361510445a29bee95336adde667c4162b76b7f4c8af3aeb3415292023"},
42 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
43 | "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
44 | "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},
45 | "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
46 | "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
47 | "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
48 | "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
49 | "phoenix": {:hex, :phoenix, "1.7.21", "14ca4f1071a5f65121217d6b57ac5712d1857e40a0833aff7a691b7870fc9a3b", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "336dce4f86cba56fed312a7d280bf2282c720abb6074bdb1b61ec8095bdd0bc9"},
50 | "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.3", "f686701b0499a07f2e3b122d84d52ff8a31f5def386e03706c916f6feddf69ef", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "909502956916a657a197f94cc1206d9a65247538de8a5e186f7537c895d95764"},
51 | "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"},
52 | "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.6", "7b1f0327f54c9eb69845fd09a77accf922f488c549a7e7b8618775eb603a62c7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "1681ab813ec26ca6915beb3414aa138f298e17721dc6a2bde9e6eb8a62360ff6"},
53 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.0", "2791fac0e2776b640192308cc90c0dbcf67843ad51387ed4ecae2038263d708d", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b3a1fa036d7eb2f956774eda7a7638cf5123f8f2175aca6d6420a7f95e598e1c"},
54 | "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.9", "4dc5e535832733df68df22f9de168b11c0c74bca65b27b088a10ac36dfb75d04", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1dccb04ec8544340e01608e108f32724458d0ac4b07e551406b3b920c40ba2e5"},
55 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
56 | "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
57 | "plug": {:hex, :plug, "1.17.0", "a0832e7af4ae0f4819e0c08dd2e7482364937aea6a8a997a679f2cbb7e026b2e", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6692046652a69a00a5a21d0b7e11fcf401064839d59d6b8787f23af55b1e6bc"},
58 | "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
59 | "qex": {:hex, :qex, "0.5.1", "0d82c0f008551d24fffb99d97f8299afcb8ea9cf99582b770bd004ed5af63fd6", [:mix], [], "hexpm", "935a39fdaf2445834b95951456559e9dc2063d0a055742c558a99987b38d6bab"},
60 | "req": {:hex, :req, "0.5.10", "a3a063eab8b7510785a467f03d30a8d95f66f5c3d9495be3474b61459c54376c", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "8a604815743f8a2d3b5de0659fa3137fa4b1cffd636ecb69b30b2b9b2c2559be"},
61 | "shmex": {:hex, :shmex, "0.5.1", "81dd209093416bf6608e66882cb7e676089307448a1afd4fc906c1f7e5b94cf4", [:mix], [{:bunch_native, "~> 0.5.0", [hex: :bunch_native, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "c29f8286891252f64c4e1dac40b217d960f7d58def597c4e606ff8fbe71ceb80"},
62 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
63 | "sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"},
64 | "tailwind": {:hex, :tailwind, "0.3.1", "a89d2835c580748c7a975ad7dd3f2ea5e63216dc16d44f9df492fbd12c094bed", [:mix], [], "hexpm", "98a45febdf4a87bc26682e1171acdedd6317d0919953c353fcd1b4f9f4b676a2"},
65 | "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
66 | "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
67 | "telemetry_poller": {:hex, :telemetry_poller, "1.2.0", "ba82e333215aed9dd2096f93bd1d13ae89d249f82760fcada0850ba33bac154b", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7216e21a6c326eb9aa44328028c34e9fd348fb53667ca837be59d0aa2a0156e8"},
68 | "thousand_island": {:hex, :thousand_island, "1.3.12", "590ff651a6d2a59ed7eabea398021749bdc664e2da33e0355e6c64e7e1a2ef93", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "55d0b1c868b513a7225892b8a8af0234d7c8981a51b0740369f3125f7c99a549"},
69 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
70 | "unifex": {:hex, :unifex, "1.2.1", "6841c170a6e16509fac30b19e4e0a19937c33155a59088b50c15fc2c36251b6b", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.4", [hex: :bundlex, repo: "hexpm", optional: false]}, {:shmex, "~> 0.5.0", [hex: :shmex, repo: "hexpm", optional: false]}], "hexpm", "8c9d2e3c48df031e9995dd16865bab3df402c0295ba3a31f38274bb5314c7d37"},
71 | "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
72 | "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"},
73 | "zarex": {:hex, :zarex, "1.0.5", "58239e3ee5d75f343262bb4df5cf466555a1c689f920e5d3651a9333972f7c7e", [:mix], [], "hexpm", "9fb72ef0567c2b2742f5119a1ba8a24a2fabb21b8d09820aefbf3e592fa9a46a"},
74 | }
75 |
--------------------------------------------------------------------------------
/lib/glitchtv_web/components/core_components.ex:
--------------------------------------------------------------------------------
1 | defmodule GlitchtvWeb.CoreComponents do
2 | @moduledoc """
3 | Provides core UI components.
4 |
5 | At first glance, this module may seem daunting, but its goal is to provide
6 | core building blocks for your application, such as modals, tables, and
7 | forms. The components consist mostly of markup and are well-documented
8 | with doc strings and declarative assigns. You may customize and style
9 | them in any way you want, based on your application growth and needs.
10 |
11 | The default components use Tailwind CSS, a utility-first CSS framework.
12 | See the [Tailwind CSS documentation](https://tailwindcss.com) to learn
13 | how to customize them or feel free to swap in another framework altogether.
14 |
15 | Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage.
16 | """
17 | use Phoenix.Component
18 |
19 | alias Phoenix.LiveView.JS
20 |
21 | @doc """
22 | Renders a modal.
23 |
24 | ## Examples
25 |
26 | <.modal id="confirm-modal">
27 | This is a modal.
28 |
29 |
30 | JS commands may be passed to the `:on_cancel` to configure
31 | the closing/cancel event, for example:
32 |
33 | <.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}>
34 | This is another modal.
35 |
36 |
37 | """
38 | attr :id, :string, required: true
39 | attr :show, :boolean, default: false
40 | attr :on_cancel, JS, default: %JS{}
41 | slot :inner_block, required: true
42 |
43 | def modal(assigns) do
44 | ~H"""
45 |
52 |
53 |
61 |
62 |
63 | <.focus_wrap
64 | id={"#{@id}-container"}
65 | phx-window-keydown={JS.exec("data-cancel", to: "##{@id}")}
66 | phx-key="escape"
67 | phx-click-away={JS.exec("data-cancel", to: "##{@id}")}
68 | class="shadow-zinc-700/10 ring-zinc-700/10 relative hidden rounded-2xl bg-white p-14 shadow-lg ring-1 transition"
69 | >
70 |
71 |
77 | <.icon name="hero-x-mark-solid" class="h-5 w-5" />
78 |
79 |
80 |
81 | {render_slot(@inner_block)}
82 |
83 |
84 |
85 |
86 |
87 |
88 | """
89 | end
90 |
91 | @doc """
92 | Renders flash notices.
93 |
94 | ## Examples
95 |
96 | <.flash kind={:info} flash={@flash} />
97 | <.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!
98 | """
99 | attr :id, :string, doc: "the optional id of flash container"
100 | attr :flash, :map, default: %{}, doc: "the map of flash messages to display"
101 | attr :title, :string, default: nil
102 | attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup"
103 | attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container"
104 |
105 | slot :inner_block, doc: "the optional inner block that renders the flash message"
106 |
107 | def flash(assigns) do
108 | assigns = assign_new(assigns, :id, fn -> "flash-#{assigns.kind}" end)
109 |
110 | ~H"""
111 |
hide("##{@id}")}
115 | role="alert"
116 | class={[
117 | "fixed top-2 right-2 mr-2 w-80 sm:w-96 z-50 rounded-lg p-3 ring-1",
118 | @kind == :info && "bg-emerald-50 text-emerald-800 ring-emerald-500 fill-cyan-900",
119 | @kind == :error && "bg-rose-50 text-rose-900 shadow-md ring-rose-500 fill-rose-900"
120 | ]}
121 | {@rest}
122 | >
123 |
124 | <.icon :if={@kind == :info} name="hero-information-circle-mini" class="h-4 w-4" />
125 | <.icon :if={@kind == :error} name="hero-exclamation-circle-mini" class="h-4 w-4" />
126 | {@title}
127 |
128 |
{msg}
129 |
130 | <.icon name="hero-x-mark-solid" class="h-5 w-5 opacity-40 group-hover:opacity-70" />
131 |
132 |
133 | """
134 | end
135 |
136 | @doc """
137 | Shows the flash group with standard titles and content.
138 |
139 | ## Examples
140 |
141 | <.flash_group flash={@flash} />
142 | """
143 | attr :flash, :map, required: true, doc: "the map of flash messages"
144 | attr :id, :string, default: "flash-group", doc: "the optional id of flash container"
145 |
146 | def flash_group(assigns) do
147 | ~H"""
148 |
149 | <.flash kind={:info} title="Success!" flash={@flash} />
150 | <.flash kind={:error} title="Error!" flash={@flash} />
151 | <.flash
152 | id="client-error"
153 | kind={:error}
154 | title="We can't find the internet"
155 | phx-disconnected={show(".phx-client-error #client-error")}
156 | phx-connected={hide("#client-error")}
157 | hidden
158 | >
159 | Attempting to reconnect <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
160 |
161 |
162 | <.flash
163 | id="server-error"
164 | kind={:error}
165 | title="Something went wrong!"
166 | phx-disconnected={show(".phx-server-error #server-error")}
167 | phx-connected={hide("#server-error")}
168 | hidden
169 | >
170 | Hang in there while we get back on track
171 | <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
172 |
173 |
174 | """
175 | end
176 |
177 | @doc """
178 | Renders a simple form.
179 |
180 | ## Examples
181 |
182 | <.simple_form for={@form} phx-change="validate" phx-submit="save">
183 | <.input field={@form[:email]} label="Email"/>
184 | <.input field={@form[:username]} label="Username" />
185 | <:actions>
186 | <.button>Save
187 |
188 |
189 | """
190 | attr :for, :any, required: true, doc: "the data structure for the form"
191 | attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
192 |
193 | attr :rest, :global,
194 | include: ~w(autocomplete name rel action enctype method novalidate target multipart),
195 | doc: "the arbitrary HTML attributes to apply to the form tag"
196 |
197 | slot :inner_block, required: true
198 | slot :actions, doc: "the slot for form actions, such as a submit button"
199 |
200 | def simple_form(assigns) do
201 | ~H"""
202 | <.form :let={f} for={@for} as={@as} {@rest}>
203 |
204 | {render_slot(@inner_block, f)}
205 |
206 | {render_slot(action, f)}
207 |
208 |
209 |
210 | """
211 | end
212 |
213 | @doc """
214 | Renders a button.
215 |
216 | ## Examples
217 |
218 | <.button>Send!
219 | <.button phx-click="go" class="ml-2">Send!
220 | """
221 | attr :type, :string, default: nil
222 | attr :class, :string, default: nil
223 | attr :rest, :global, include: ~w(disabled form name value)
224 |
225 | slot :inner_block, required: true
226 |
227 | def button(assigns) do
228 | ~H"""
229 |
238 | {render_slot(@inner_block)}
239 |
240 | """
241 | end
242 |
243 | @doc """
244 | Renders an input with label and error messages.
245 |
246 | A `Phoenix.HTML.FormField` may be passed as argument,
247 | which is used to retrieve the input name, id, and values.
248 | Otherwise all attributes may be passed explicitly.
249 |
250 | ## Types
251 |
252 | This function accepts all HTML input types, considering that:
253 |
254 | * You may also set `type="select"` to render a `
` tag
255 |
256 | * `type="checkbox"` is used exclusively to render boolean values
257 |
258 | * For live file uploads, see `Phoenix.Component.live_file_input/1`
259 |
260 | See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input
261 | for more information. Unsupported types, such as hidden and radio,
262 | are best written directly in your templates.
263 |
264 | ## Examples
265 |
266 | <.input field={@form[:email]} type="email" />
267 | <.input name="my-input" errors={["oh no!"]} />
268 | """
269 | attr :id, :any, default: nil
270 | attr :name, :any
271 | attr :label, :string, default: nil
272 | attr :value, :any
273 |
274 | attr :type, :string,
275 | default: "text",
276 | values: ~w(checkbox color date datetime-local email file month number password
277 | range search select tel text textarea time url week)
278 |
279 | attr :field, Phoenix.HTML.FormField,
280 | doc: "a form field struct retrieved from the form, for example: @form[:email]"
281 |
282 | attr :errors, :list, default: []
283 | attr :checked, :boolean, doc: "the checked flag for checkbox inputs"
284 | attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
285 | attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
286 | attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
287 |
288 | attr :rest, :global,
289 | include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength
290 | multiple pattern placeholder readonly required rows size step)
291 |
292 | def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
293 | errors = if Phoenix.Component.used_input?(field), do: field.errors, else: []
294 |
295 | assigns
296 | |> assign(field: nil, id: assigns.id || field.id)
297 | |> assign(:errors, Enum.map(errors, &translate_error(&1)))
298 | |> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
299 | |> assign_new(:value, fn -> field.value end)
300 | |> input()
301 | end
302 |
303 | def input(%{type: "checkbox"} = assigns) do
304 | assigns =
305 | assign_new(assigns, :checked, fn ->
306 | Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value])
307 | end)
308 |
309 | ~H"""
310 |
311 |
312 |
313 |
322 | {@label}
323 |
324 | <.error :for={msg <- @errors}>{msg}
325 |
326 | """
327 | end
328 |
329 | def input(%{type: "select"} = assigns) do
330 | ~H"""
331 |
332 | <.label for={@id}>{@label}
333 |
340 | {@prompt}
341 | {Phoenix.HTML.Form.options_for_select(@options, @value)}
342 |
343 | <.error :for={msg <- @errors}>{msg}
344 |
345 | """
346 | end
347 |
348 | def input(%{type: "textarea"} = assigns) do
349 | ~H"""
350 |
351 | <.label for={@id}>{@label}
352 | {Phoenix.HTML.Form.normalize_value("textarea", @value)}
362 | <.error :for={msg <- @errors}>{msg}
363 |
364 | """
365 | end
366 |
367 | # All other inputs text, datetime-local, url, password, etc. are handled here...
368 | def input(assigns) do
369 | ~H"""
370 |
371 | <.label for={@id}>{@label}
372 |
384 | <.error :for={msg <- @errors}>{msg}
385 |
386 | """
387 | end
388 |
389 | @doc """
390 | Renders a label.
391 | """
392 | attr :for, :string, default: nil
393 | slot :inner_block, required: true
394 |
395 | def label(assigns) do
396 | ~H"""
397 |
398 | {render_slot(@inner_block)}
399 |
400 | """
401 | end
402 |
403 | @doc """
404 | Generates a generic error message.
405 | """
406 | slot :inner_block, required: true
407 |
408 | def error(assigns) do
409 | ~H"""
410 |
411 | <.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" />
412 | {render_slot(@inner_block)}
413 |
414 | """
415 | end
416 |
417 | @doc """
418 | Renders a header with title.
419 | """
420 | attr :class, :string, default: nil
421 |
422 | slot :inner_block, required: true
423 | slot :subtitle
424 | slot :actions
425 |
426 | def header(assigns) do
427 | ~H"""
428 |
439 | """
440 | end
441 |
442 | @doc ~S"""
443 | Renders a table with generic styling.
444 |
445 | ## Examples
446 |
447 | <.table id="users" rows={@users}>
448 | <:col :let={user} label="id">{user.id}
449 | <:col :let={user} label="username">{user.username}
450 |
451 | """
452 | attr :id, :string, required: true
453 | attr :rows, :list, required: true
454 | attr :row_id, :any, default: nil, doc: "the function for generating the row id"
455 | attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row"
456 |
457 | attr :row_item, :any,
458 | default: &Function.identity/1,
459 | doc: "the function for mapping each row before calling the :col and :action slots"
460 |
461 | slot :col, required: true do
462 | attr :label, :string
463 | end
464 |
465 | slot :action, doc: "the slot for showing user actions in the last table column"
466 |
467 | def table(assigns) do
468 | assigns =
469 | with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
470 | assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end)
471 | end
472 |
473 | ~H"""
474 |
475 |
476 |
477 |
478 | {col[:label]}
479 |
480 | Actions
481 |
482 |
483 |
484 |
489 |
490 |
495 |
496 |
497 |
498 | {render_slot(col, @row_item.(row))}
499 |
500 |
501 |
502 |
503 |
504 |
505 |
509 | {render_slot(action, @row_item.(row))}
510 |
511 |
512 |
513 |
514 |
515 |
516 |
517 | """
518 | end
519 |
520 | @doc """
521 | Renders a data list.
522 |
523 | ## Examples
524 |
525 | <.list>
526 | <:item title="Title">{@post.title}
527 | <:item title="Views">{@post.views}
528 |
529 | """
530 | slot :item, required: true do
531 | attr :title, :string, required: true
532 | end
533 |
534 | def list(assigns) do
535 | ~H"""
536 |
537 |
538 |
539 |
{item.title}
540 | {render_slot(item)}
541 |
542 |
543 |
544 | """
545 | end
546 |
547 | @doc """
548 | Renders a back navigation link.
549 |
550 | ## Examples
551 |
552 | <.back navigate={~p"/posts"}>Back to posts
553 | """
554 | attr :navigate, :any, required: true
555 | slot :inner_block, required: true
556 |
557 | def back(assigns) do
558 | ~H"""
559 |
560 | <.link
561 | navigate={@navigate}
562 | class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
563 | >
564 | <.icon name="hero-arrow-left-solid" class="h-3 w-3" />
565 | {render_slot(@inner_block)}
566 |
567 |
568 | """
569 | end
570 |
571 | @doc """
572 | Renders a [Heroicon](https://heroicons.com).
573 |
574 | Heroicons come in three styles – outline, solid, and mini.
575 | By default, the outline style is used, but solid and mini may
576 | be applied by using the `-solid` and `-mini` suffix.
577 |
578 | You can customize the size and colors of the icons by setting
579 | width, height, and background color classes.
580 |
581 | Icons are extracted from the `deps/heroicons` directory and bundled within
582 | your compiled app.css by the plugin in your `assets/tailwind.config.js`.
583 |
584 | ## Examples
585 |
586 | <.icon name="hero-x-mark-solid" />
587 | <.icon name="hero-arrow-path" class="ml-1 w-3 h-3 animate-spin" />
588 | """
589 | attr :name, :string, required: true
590 | attr :class, :string, default: nil
591 |
592 | def icon(%{name: "hero-" <> _} = assigns) do
593 | ~H"""
594 |
595 | """
596 | end
597 |
598 | ## JS Commands
599 |
600 | def show(js \\ %JS{}, selector) do
601 | JS.show(js,
602 | to: selector,
603 | time: 300,
604 | transition:
605 | {"transition-all transform ease-out duration-300",
606 | "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
607 | "opacity-100 translate-y-0 sm:scale-100"}
608 | )
609 | end
610 |
611 | def hide(js \\ %JS{}, selector) do
612 | JS.hide(js,
613 | to: selector,
614 | time: 200,
615 | transition:
616 | {"transition-all transform ease-in duration-200",
617 | "opacity-100 translate-y-0 sm:scale-100",
618 | "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"}
619 | )
620 | end
621 |
622 | def show_modal(js \\ %JS{}, id) when is_binary(id) do
623 | js
624 | |> JS.show(to: "##{id}")
625 | |> JS.show(
626 | to: "##{id}-bg",
627 | time: 300,
628 | transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"}
629 | )
630 | |> show("##{id}-container")
631 | |> JS.add_class("overflow-hidden", to: "body")
632 | |> JS.focus_first(to: "##{id}-content")
633 | end
634 |
635 | def hide_modal(js \\ %JS{}, id) do
636 | js
637 | |> JS.hide(
638 | to: "##{id}-bg",
639 | transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"}
640 | )
641 | |> hide("##{id}-container")
642 | |> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"})
643 | |> JS.remove_class("overflow-hidden", to: "body")
644 | |> JS.pop_focus()
645 | end
646 |
647 | @doc """
648 | Translates an error message using gettext.
649 | """
650 | def translate_error({msg, opts}) do
651 | # You can make use of gettext to translate error messages by
652 | # uncommenting and adjusting the following code:
653 |
654 | # if count = opts[:count] do
655 | # Gettext.dngettext(GlitchtvWeb.Gettext, "errors", msg, msg, count, opts)
656 | # else
657 | # Gettext.dgettext(GlitchtvWeb.Gettext, "errors", msg, opts)
658 | # end
659 |
660 | Enum.reduce(opts, msg, fn {key, value}, acc ->
661 | String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end)
662 | end)
663 | end
664 |
665 | @doc """
666 | Translates the errors for a field from a keyword list of errors.
667 | """
668 | def translate_errors(errors, field) when is_list(errors) do
669 | for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
670 | end
671 |
672 | attr :class, :string, default: nil
673 | slot :inner_block, required: true
674 |
675 | def dropping(assigns) do
676 | ~H"""
677 |
681 | {render_slot(@inner_block)}
682 |
683 | """
684 | end
685 |
686 | def share_button(assigns) do
687 | ~H"""
688 |
693 | Share <.icon name="hero-share" class="fill-indigo-800 w-5 h-5" />
694 |
695 | """
696 | end
697 | end
698 |
--------------------------------------------------------------------------------