diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 9d5301f..3c7686e 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,6 +3,7 @@ "redhat.vscode-yaml", "elixir-lsp.elixir-ls", "jetify.devbox", - "redhat.ansible" + "redhat.ansible", + "dotjoshjohnson.xml" ] } \ No newline at end of file diff --git a/apps/bright/assets/css/app.scss b/apps/bright/assets/css/app.scss index 861a6b9..794863f 100644 --- a/apps/bright/assets/css/app.scss +++ b/apps/bright/assets/css/app.scss @@ -1,4 +1,3 @@ - @import "bulma"; @import "variables"; @@ -9,11 +8,16 @@ -.is-unclickable { +.is-unclickable { cursor: not-allowed } .phx-click-loading { opacity: 15%; +} + + +svg { + color: "lime" } \ No newline at end of file diff --git a/apps/bright/assets/static/127.jpg b/apps/bright/assets/static/127.jpg deleted file mode 100644 index 1553874..0000000 Binary files a/apps/bright/assets/static/127.jpg and /dev/null differ diff --git a/apps/bright/assets/static/favicon.ico b/apps/bright/assets/static/favicon222.ico similarity index 100% rename from apps/bright/assets/static/favicon.ico rename to apps/bright/assets/static/favicon222.ico diff --git a/apps/bright/config/config.exs b/apps/bright/config/config.exs index 8772e63..f0a8de2 100644 --- a/apps/bright/config/config.exs +++ b/apps/bright/config/config.exs @@ -32,7 +32,8 @@ config :bright, Oban, {Oban.Plugins.Lifeline, rescue_after: :timer.minutes(30)}, {Oban.Plugins.Cron, crontab: [ - {"*/15 * * * *", Bright.ObanWorkers.ReadPosts} + {"*/10 * * * *", Bright.ObanWorkers.ScrapePosts}, + {"*/1 * * * *", Bright.ObanWorkers.ProcessPosts} ]} ] diff --git a/apps/bright/config/runtime.exs b/apps/bright/config/runtime.exs index 89b0f7e..b5cbed2 100644 --- a/apps/bright/config/runtime.exs +++ b/apps/bright/config/runtime.exs @@ -28,7 +28,8 @@ config :bright, aws_region: System.get_env("AWS_REGION"), public_s3_endpoint: System.get_env("PUBLIC_S3_ENDPOINT"), site_url: System.get_env("SITE_URL"), - cache_dir: System.get_env("CACHE_DIR") + cache_dir: System.get_env("CACHE_DIR"), + vultr_ai_api_key: System.get_env("VULTR_AI_API_KEY") config :bright, :torrent, tracker_url: System.get_env("TRACKER_URL"), diff --git a/apps/bright/config/test.exs b/apps/bright/config/test.exs index 65876e3..2c2ae5c 100644 --- a/apps/bright/config/test.exs +++ b/apps/bright/config/test.exs @@ -10,11 +10,11 @@ config :bcrypt_elixir, :log_rounds, 1 # Run `mix help test` for more information. config :bright, Bright.Repo, database: System.get_env("DB_NAME", "bright"), + # database: "bright_test#{System.get_env("MIX_TEST_PARTITION")}", hostname: System.get_env("DB_HOST", "localhost"), username: System.get_env("DB_USER", "postgres"), password: System.get_env("DB_PASS", "password"), port: System.get_env("DB_PORT", "5433"), - # database: "bright_test#{System.get_env("MIX_TEST_PARTITION")}", pool: Ecto.Adapters.SQL.Sandbox, pool_size: System.schedulers_online() * 4 diff --git a/apps/bright/lib/bright/oban_workers/README.md b/apps/bright/lib/bright/oban_workers/README.md new file mode 100644 index 0000000..50e1d19 --- /dev/null +++ b/apps/bright/lib/bright/oban_workers/README.md @@ -0,0 +1,10 @@ + +Please name jobs as if you were speaking with a human. + +## Bad example + + posts_read.ex + +## Good Example + + read_posts.ex \ No newline at end of file diff --git a/apps/bright/lib/bright/oban_workers/process_posts.ex b/apps/bright/lib/bright/oban_workers/process_posts.ex new file mode 100644 index 0000000..0afae66 --- /dev/null +++ b/apps/bright/lib/bright/oban_workers/process_posts.ex @@ -0,0 +1,141 @@ +defmodule Bright.ObanWorkers.ProcessPosts do + @moduledoc """ + Read the social media posts we have in the db, and create Stream records for each post with a livestream invitation. + """ + + use Oban.Worker, queue: :default, max_attempts: 3 + + alias Bright.Vtubers.Vtuber + alias Bright.Repo + alias Bright.Socials.XPost + alias Bright.Streams.Stream + alias Bright.Platforms.Platform + import Ecto.Query + + require Logger + + # 1. get unprocessed posts + # 2. process each post + # * get platforms mentioned + # * find any reason to determine the post is NOT a NSFW livestream announcement + # 3. for each nsfw livestream announcement post, create a stream + @impl Oban.Worker + def perform(%Oban.Job{args: %{}}) do + Logger.info(">>>> Process Posts is performing.") + + known_platforms = Repo.all(Platform) + + {num, nil} = + get_unprocessed_posts() + |> then(fn posts -> + if posts == [] do + Logger.info("No unprocessed posts found") + else + Logger.debug("#{length(posts)} unprocessed posts found.") + Enum.each(posts, &process_post(&1, known_platforms)) + mark_posts_as_processed(posts) + end + end) + + {:ok, num} + end + + def process_post(post, known_platforms) do + with platforms <- XPost.get_platforms_mentioned(post, known_platforms), + true <- is_nsfw_live_annoucement?(post, platforms, known_platforms), + {:ok, _stream} <- create_stream(post, platforms) do + :ok + else + _ -> :ok + end + end + + def get_unprocessed_posts() do + XPost + |> where([p], is_nil(p.processed_at)) + |> preload(:vtuber) + |> Repo.all() + end + + @doc """ + create streams for each announcement post + """ + def create_stream(post, platforms) do + vtuber = post.vtuber + date = post |> Map.get(:date) + title = "#{post.vtuber.display_name} #{date}" + + Logger.debug( + "WE ARE CREATING A Stream with platforms=#{inspect(platforms)} title=#{inspect(title)} with date=#{inspect(date)} post=#{inspect(post)}" + ) + + changeset = + %Stream{} + |> Stream.changeset(%{title: title, date: date}) + |> Ecto.Changeset.put_assoc(:platforms, platforms) + |> Ecto.Changeset.put_assoc(:vtubers, [vtuber]) + |> Ecto.Changeset.put_assoc(:x_post, post) + + case Repo.insert(changeset) do + {:ok, stream} -> + Logger.info("Created stream: #{inspect(stream)}") + + {:error, %Ecto.Changeset{errors: [date: {"has already been taken", _}]}} -> + Logger.warn("Stream #{title} already exists. Skipping...") + + {:error, changeset} -> + Logger.error( + "Failed to create stream: #{inspect(changeset)} #{inspect(changeset.errors)}" + ) + end + end + + defp mark_posts_as_processed(posts) when length(posts) > 0 do + post_ids = Enum.map(posts, & &1.id) + + from(p in XPost, where: p.id in ^post_ids) + |> Repo.update_all(set: [processed_at: DateTime.utc_now(:second)]) + end + + # No posts to update + defp mark_posts_as_processed(_), do: :ok + + @doc """ + Is the post a valid NSFW livestream announcement? + + Eligibility requirements. + To be considered a NSFW live announcement, a post must satisfy all the following conditions. + + * The post is authored by the lewdtuber + * The post mentions a NSFW platform + * The post does not contain any URLs to SFW streaming platforms. + + """ + def is_nsfw_live_annoucement?(%XPost{vtuber: vtuber} = post, platforms, known_platforms) do + Logger.debug("Checking if post is NSFW live announcement: #{inspect(post)}") + + nsfw_platforms = Enum.filter(known_platforms, & &1.nsfw?) + sfw_platforms = Enum.reject(known_platforms, & &1.nsfw?) + + conditions = [ + {:authored_by_vtuber?, not is_nil(vtuber)}, + {:contains_nsfw_link?, + Enum.any?(platforms, fn plat -> Enum.any?(nsfw_platforms, &match_platform?(plat, &1)) end)}, + {:no_sfw_link?, + not Enum.any?(platforms, fn plat -> + Enum.any?(sfw_platforms, &match_platform?(plat, &1)) + end)} + ] + + Enum.reduce_while(conditions, true, fn {label, condition}, _acc -> + if condition do + {:cont, true} + else + Logger.debug("NSFW announcement check failed at: #{label}") + {:halt, false} + end + end) + end + + defp match_platform?(plat, platform), do: String.contains?(plat, &URI.parse(&1.url).hostname) +end diff --git a/apps/bright/lib/bright/oban_workers/save_posts.ex b/apps/bright/lib/bright/oban_workers/scrape_posts.ex similarity index 94% rename from apps/bright/lib/bright/oban_workers/save_posts.ex rename to apps/bright/lib/bright/oban_workers/scrape_posts.ex index 2432561..e0c422d 100644 --- a/apps/bright/lib/bright/oban_workers/save_posts.ex +++ b/apps/bright/lib/bright/oban_workers/scrape_posts.ex @@ -1,8 +1,6 @@ -defmodule Bright.ObanWorkers.ReadPosts do +defmodule Bright.ObanWorkers.ScrapePosts do @moduledoc """ Read a vtuber's social media feed and save the posts to the db - - * [ ] X """ alias Bright.Vtubers.Vtuber @@ -15,7 +13,7 @@ defmodule Bright.ObanWorkers.ReadPosts do @impl Oban.Worker def perform(%Oban.Job{args: %{}}) do - Logger.info(">>>> Save Posts is performing.") + Logger.info(">>>> Scrape Posts is performing.") vtubers = Repo.all(Vtuber) Logger.debug("there are #{length(vtubers)} vtubers.") diff --git a/apps/bright/lib/bright/platforms.ex b/apps/bright/lib/bright/platforms.ex index 0b99102..14f95a4 100644 --- a/apps/bright/lib/bright/platforms.ex +++ b/apps/bright/lib/bright/platforms.ex @@ -5,7 +5,7 @@ defmodule Bright.Platforms do import Ecto.Query, warn: false alias Bright.Repo - + alias Bright.Platforms.PlatformAlias alias Bright.Platforms.Platform @doc """ @@ -19,6 +19,7 @@ defmodule Bright.Platforms do """ def list_platforms do Repo.all(Platform) + |> Repo.preload([:platform_aliases]) end @doc """ @@ -35,7 +36,11 @@ defmodule Bright.Platforms do ** (Ecto.NoResultsError) """ - def get_platform!(id), do: Repo.get!(Platform, id) + def get_platform!(id) do + Platform + |> Repo.get!(Platform, id) + |> Repo.preload([:platform_aliases]) + end @doc """ Creates a platform. @@ -69,6 +74,7 @@ defmodule Bright.Platforms do """ def update_platform(%Platform{} = platform, attrs) do platform + |> Repo.preload([:platform_aliases]) |> Platform.changeset(attrs) |> Repo.update() end @@ -101,4 +107,22 @@ defmodule Bright.Platforms do def change_platform(%Platform{} = platform, attrs \\ %{}) do Platform.changeset(platform, attrs) end + + @doc """ + Creates a platform alias. + + ## Examples + + iex> create_platform_alias(%{field: value}) + {:ok, %PlatformAlias{}} + + iex> create_platform(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_platform_alias(attrs \\ %{}) do + %PlatformAlias{} + |> PlatformAlias.changeset(attrs) + |> Repo.insert() + end end diff --git a/apps/bright/lib/bright/platforms/platform.ex b/apps/bright/lib/bright/platforms/platform.ex index d39237b..191efe2 100644 --- a/apps/bright/lib/bright/platforms/platform.ex +++ b/apps/bright/lib/bright/platforms/platform.ex @@ -4,8 +4,11 @@ defmodule Bright.Platforms.Platform do schema "platforms" do field :name, :string + field :slug, :string field :url, :string - field :icon, :string + field :nsfw, :boolean + + has_many :platform_aliases, Bright.Platforms.PlatformAlias timestamps(type: :utc_datetime) end @@ -13,7 +16,7 @@ defmodule Bright.Platforms.Platform do @doc false def changeset(platform, attrs) do platform - |> cast(attrs, [:name, :url, :icon]) - |> validate_required([:name, :url, :icon]) + |> cast(attrs, [:name, :url, :slug]) + |> validate_required([:name, :url, :slug]) end end diff --git a/apps/bright/lib/bright/platforms/platform_alias.ex b/apps/bright/lib/bright/platforms/platform_alias.ex new file mode 100644 index 0000000..886f4f1 --- /dev/null +++ b/apps/bright/lib/bright/platforms/platform_alias.ex @@ -0,0 +1,18 @@ +defmodule Bright.Platforms.PlatformAlias do + use Ecto.Schema + import Ecto.Changeset + + schema "platform_aliases" do + field :url, :string + field :platform_id, :id + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(platform_alias, attrs) do + platform_alias + |> cast(attrs, [:url, :platform_id]) + |> validate_required([:url, :platform_id]) + end +end diff --git a/apps/bright/lib/bright/platforms/platform_alias.ex.bak b/apps/bright/lib/bright/platforms/platform_alias.ex.bak new file mode 100644 index 0000000..348dbb5 --- /dev/null +++ b/apps/bright/lib/bright/platforms/platform_alias.ex.bak @@ -0,0 +1,19 @@ +defmodule Bright.Platforms.PlatformAlias do + use Ecto.Schema + import Ecto.Changeset + + schema "platform_aliases" do + field :url, :string + + belongs_to :platform, Bright.Platforms.Platform + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(platform_alias, attrs) do + platform_alias + |> cast(attrs, [:url, :platform_id]) + |> validate_required([:url, :platform_id]) + end +end diff --git a/apps/bright/lib/bright/socials/x_post.ex b/apps/bright/lib/bright/socials/x_post.ex index 30c1d5e..70f9245 100644 --- a/apps/bright/lib/bright/socials/x_post.ex +++ b/apps/bright/lib/bright/socials/x_post.ex @@ -1,23 +1,24 @@ defmodule Bright.Socials.XPost do use Ecto.Schema - import Ecto.Changeset + import Ecto.{Changeset, Query} + alias Bright.Repo alias Bright.Vtubers.Vtuber - alias Bright.Socials.RSSParser + alias Bright.Socials.{XPost, RSSParser} + alias Bright.Platforms.Platform alias Quinn require Logger - @livestream_domains ["chaturbate.com", "fansly.com", "onlyfans.com"] - @doc """ We cache the posts in the db so it's clear which tweets we've read and which ones we haven't. - We only parse tweets which haven't been cached. + The idea is to process only uncached posts. """ schema "x_posts" do field :raw, :string field :url, :string field :date, :utc_datetime - field :is_invitation, :boolean + field :processed_at, :utc_datetime + belongs_to :stream, Stream belongs_to :vtuber, Bright.Vtubers.Vtuber timestamps(type: :utc_datetime) @@ -26,9 +27,10 @@ defmodule Bright.Socials.XPost do @doc false def changeset(post, attrs) do post - |> cast(attrs, [:raw, :url, :date, :vtuber_id]) - |> validate_required([:raw, :url, :date]) - |> unique_constraint([:date, :url]) + |> cast(attrs, [:raw, :url, :date, :vtuber_id, :processed_at]) + |> validate_required([:raw, :url, :date, :vtuber_id]) + |> unique_constraint(:date) + |> unique_constraint(:url) end @doc """ @@ -64,19 +66,57 @@ defmodule Bright.Socials.XPost do end end - @doc """ - save the posts to the db - """ - def save_posts(posts) do - Logger.debug("@todo implement save_posts()") + def extract_hostname(url) do + uri = URI.parse(url) + uri.host || "" + end + + def includes_alias?(%XPost{raw: raw}, platform), do: includes_alias?(raw, platform) + + def includes_alias?(raw, platform) do + case Map.get(platform, :platform_aliases, []) do + [] -> false + aliases -> Enum.any?(aliases, fn alias -> raw =~ extract_hostname(alias.url) end) + end end @doc """ - return true if there is a livestream invitation in the post, false otherwise + Checks if the given raw text or XPost includes a reference to the specified platform. + + The function checks for matches against the platform's hostname or its aliases. + + ## Parameters + - raw_text: The raw text or XPost to search within. + - platform: The %Platform{} struct containing the URL and aliases to match against. + + ## Returns + - `true` if the platform's hostname or any of its aliases are found in the raw text. + - `false` otherwise. """ - def find_livestream_invitation(%__MODULE__{raw: raw}) do - Enum.any?(@livestream_domains, fn domain -> - String.downcase(raw) =~ ~r/#{domain}\/[^\s]*/ - end) + def includes_platform?(%XPost{raw: raw}, platform) do + includes_platform?(raw, platform) + end + + def includes_platform?(raw_text, %Platform{url: url} = platform) + when is_binary(raw_text) and is_binary(url) do + hostname_match = raw_text =~ extract_hostname(url) + alias_match = includes_alias?(raw_text, platform) + hostname_match || alias_match + end + + def includes_platform?(_, _), do: false + + def get_platforms_mentioned(%XPost{raw: raw}, [%Platform{} = platforms]) do + get_platforms_mentioned(raw, platforms) + end + + def get_platforms_mentioned(raw_text, platforms) do + Enum.filter(platforms, &includes_platform?(raw_text, &1)) + end +end + +defimpl Phoenix.HTML.Safe, for: Bright.Socials.XPost do + def to_iodata(x_post) do + Phoenix.HTML.Safe.to_iodata("#{x_post.raw} -- #{x_post.url} -- #{x_post.date}") end end diff --git a/apps/bright/lib/bright/streams.ex b/apps/bright/lib/bright/streams.ex index bd2414a..5d765cd 100644 --- a/apps/bright/lib/bright/streams.ex +++ b/apps/bright/lib/bright/streams.ex @@ -11,6 +11,7 @@ defmodule Bright.Streams do alias Bright.Vtubers.Vtuber alias Bright.Tags.Tag alias Bright.Platforms.Platform + alias Bright.Socials.XPost alias Bright.{ Cache, @@ -34,7 +35,7 @@ defmodule Bright.Streams do def list_streams do Stream |> Repo.all() - |> Repo.preload([:tags, :vods, :vtubers, :platforms]) + |> Repo.preload([:tags, :vods, :vtubers, :platforms, :x_post]) end @doc """ @@ -54,7 +55,7 @@ defmodule Bright.Streams do def get_stream!(id) do Stream |> Repo.get!(id) - |> Repo.preload([:tags, :vods, :vtubers, :platforms]) + |> Repo.preload([:tags, :vods, :vtubers, :platforms, :x_post]) end @doc """ @@ -126,7 +127,7 @@ defmodule Bright.Streams do platforms = list_platforms_by_id(attrs["platform_ids"]) stream - |> Repo.preload([:tags, :vods, :vtubers]) + |> Repo.preload([:tags, :vods, :vtubers, :x_post]) |> Stream.changeset(attrs) |> Ecto.Changeset.put_assoc(:tags, tags) |> Ecto.Changeset.put_assoc(:vods, vods) diff --git a/apps/bright/lib/bright/streams/stream.ex b/apps/bright/lib/bright/streams/stream.ex index 10828c2..44a9a00 100644 --- a/apps/bright/lib/bright/streams/stream.ex +++ b/apps/bright/lib/bright/streams/stream.ex @@ -4,12 +4,14 @@ defmodule Bright.Streams.Stream do alias Bright.Tags.Tag alias Bright.Vtubers.Vtuber alias Bright.Platforms.Platform + alias Bright.Socials.XPost schema "streams" do field :date, :utc_datetime field :title, :string field :notes, :string + has_one :x_post, XPost many_to_many :tags, Tag, join_through: "streams_tags", on_replace: :delete many_to_many :vtubers, Vtuber, join_through: "streams_vtubers", on_replace: :delete many_to_many :platforms, Platform, join_through: "streams_platforms", on_replace: :delete @@ -24,5 +26,6 @@ defmodule Bright.Streams.Stream do stream |> cast(attrs, [:title, :notes, :date]) |> validate_required([:title, :date]) + |> unique_constraint([:date]) end end diff --git a/apps/bright/lib/bright/vultr_ai.ex b/apps/bright/lib/bright/vultr_ai.ex new file mode 100644 index 0000000..83064fb --- /dev/null +++ b/apps/bright/lib/bright/vultr_ai.ex @@ -0,0 +1,131 @@ +defmodule Bright.VultrAI do + require Logger + @model "mistral-7b-v0.3" + @chat_endpoint "https://api.vultrinference.com/v1/chat/completions" + + @doc """ + This is not in-use due to less than stellar results. Keeping for future reference. + """ + def parse_social_post(raw_text, known_platforms) do + system_prompt = """ + You are a social media post parser specializing in identifying livestream invitations and the platforms they are taking place on. + + Analyze the following tweet and extract relevant information according to the JSON schema provided. Your response must be **valid JSON**. + + ## **Rules:** + 1. **Title:** Use a short, relevant snippet from the tweet that represents the livestream event. If no livestream is mentioned, set the title as an empty string (`""`). + 2. **Platforms:** Identify any livestream platforms mentioned in the tweet from this predefined list: + #{known_platforms} + - If none are found, return an empty array (`[]`). + + ## **Expected Response Schema** + + #{expected_schema(known_platforms)} + + """ + + user_prompt = raw_text + + request(@chat_endpoint, @model, system_prompt, user_prompt) + end + + def expected_schema(known_platforms) do + %{ + "type" => "object", + "properties" => %{ + "title" => %{ + "type" => "string", + "minLength" => 0 + }, + "platforms" => %{ + "type" => "array", + "items" => %{ + "type" => "string", + "enum" => known_platforms + }, + "minItems" => 0, + "uniqueItems" => true + } + }, + "required" => ["title", "platforms"], + "additionalProperties" => false + } + |> Jason.encode!() + end + + def request(endpoint, model, system_prompt, user_prompt) do + api_key = Application.get_env(:bright, :vultr_ai_api_key) + + headers = [ + {"Authorization", "Bearer #{api_key}"}, + {"Content-Type", "application/json"}, + {"Accept", "Application/json; Charset=utf-8"} + ] + + body = + %{ + messages: [ + %{ + role: "system", + content: system_prompt + }, + %{ + role: "user", + content: user_prompt + } + ], + model: model, + stream: false, + max_tokens: 512, + n: 1, + seed: 0, + temperature: 1, + top_p: 1, + frequency_penalty: 0, + presence_penalty: 0, + logprobs: false + } + |> Jason.encode!() + + case(HTTPoison.post(endpoint, body, headers)) do + {:ok, %HTTPoison.Response{status_code: 200, body: response_body}} -> + Logger.info("Successful VultrAI") + + parse_response(response_body) + + {:ok, %HTTPoison.Response{status_code: status_code, body: error_body}} -> + Logger.error("Failed VultrAI status=#{status_code}, error=#{error_body}") + + {:error, %{status: status_code, body: error_body}} + + {:error, %HTTPoison.Error{reason: reason}} -> + Logger.error("VultrAI HTTP request failed, reason=#{inspect(reason)}") + + {:error, reason} + end + end + + defp parse_response(response_body) do + with {:ok, decoded} <- Jason.decode(response_body), + {:ok, raw_content} <- extract_content(decoded), + {:ok, parsed_content} <- Jason.decode(raw_content) do + Logger.info("Successful VultrAI response") + Logger.debug(parsed_content) + {:ok, parsed_content} + else + error -> + log_and_return_error("Failed to parse response", error) + end + end + + defp extract_content(%{"choices" => [%{"message" => %{"content" => content}} | _]}) do + {:ok, content} + end + + defp extract_content(_), do: {:error, :invalid_response_format} + + defp log_and_return_error(message, details) do + Logger.error("#{message}: #{inspect(details)}") + {:error, details} + end +end diff --git a/apps/bright/lib/bright_web.ex b/apps/bright/lib/bright_web.ex index 32794bb..e80a48b 100644 --- a/apps/bright/lib/bright_web.ex +++ b/apps/bright/lib/bright_web.ex @@ -85,6 +85,7 @@ defmodule BrightWeb do import Phoenix.HTML # Core UI components and translation import BrightWeb.CoreComponents + import BrightWeb.SVGIcon import BrightWeb.Gettext # Shortcut for generating JS commands diff --git a/apps/bright/lib/bright_web/components/core_components.ex b/apps/bright/lib/bright_web/components/core_components.ex index f8377e9..5cd473e 100644 --- a/apps/bright/lib/bright_web/components/core_components.ex +++ b/apps/bright/lib/bright_web/components/core_components.ex @@ -18,6 +18,7 @@ defmodule BrightWeb.CoreComponents do use Gettext, backend: BrightWeb.Gettext alias Phoenix.LiveView.JS + import BrightWeb.SVGIcon @doc """ Renders an external link. @@ -181,7 +182,7 @@ defmodule BrightWeb.CoreComponents do > {gettext("Attempting to reconnect")} - <.icon name="academic_cap" class="h-4 w-4" /> + <.icon name="graduation_cap" class="h-4 w-4" /> <.flash @@ -594,7 +595,7 @@ defmodule BrightWeb.CoreComponents do class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700" > - <.icon name="arrow-left" class="icon" /> + <.icon name="arrow_left" class="icon" /> {render_slot(@inner_block)} @@ -602,36 +603,36 @@ defmodule BrightWeb.CoreComponents do """ end - @doc """ - Renders a [FontAwesom Icon](https://fontawesome.com/). + # @doc """ + # Renders a [FontAwesome Icon](https://fontawesome.com/). - Icons are extracted from the `assets` directory and bundled within - your compiled app.css by SASS. + # Icons are extracted from the `assets` directory and bundled within + # your compiled app.css by SASS. - ## Examples + # ## Examples - <.icon name="shopping-cart" /> - <.icon name="home" style="solid" /> - <.icon name="spinner" class="ml-1 w-3 h-3 animate-spin" /> - """ + # <.icon name="shopping-cart" /> + # <.icon name="home" style="solid" /> + # <.icon name="spinner" class="ml-1 w-3 h-3 animate-spin" /> + # """ + + # # attr :name, :string, required: true + # # attr :class, :string, default: nil + + # attr :rest, :global, + # doc: "the arbitrary HTML attributes for the svg container", + # include: ~w(fill stroke stroke-width) # attr :name, :string, required: true - # attr :class, :string, default: nil + # # can be "solid", "brands" + # attr :style, :string, default: "solid" + # attr :class, :string, default: "" - attr :rest, :global, - doc: "the arbitrary HTML attributes for the svg container", - include: ~w(fill stroke stroke-width) - - attr :name, :string, required: true - # can be "solid", "brands" - attr :style, :string, default: "solid" - attr :class, :string, default: "" - - def icon(assigns) do - ~H""" - - """ - end + # def icon(assigns) do + # ~H""" + # + # """ + # end defp style_prefix("solid"), do: "s" defp style_prefix("brands"), do: "b" diff --git a/apps/bright/lib/bright_web/components/layouts/app.html.heex b/apps/bright/lib/bright_web/components/layouts/app.html.heex index 6a5f494..9113baf 100644 --- a/apps/bright/lib/bright_web/components/layouts/app.html.heex +++ b/apps/bright/lib/bright_web/components/layouts/app.html.heex @@ -51,7 +51,7 @@
diff --git a/apps/bright/lib/bright_web/components/svg_icon.ex b/apps/bright/lib/bright_web/components/svg_icon.ex new file mode 100644 index 0000000..6e4e435 --- /dev/null +++ b/apps/bright/lib/bright_web/components/svg_icon.ex @@ -0,0 +1,122 @@ +defmodule BrightWeb.SVGIcon do + @moduledoc """ + This package adds a convenient way of using svg icons with your Phoenix, Phoenix LiveView and Surface applications. + + greets https://github.com/miguel-s/ex_heroicons/blob/main/lib/heroicons.ex + + ## Usage + +
A platform built by fans, for fans, dedicated to preserving the moments that matter in the world of R-18 VTuber live streaming. It all started with a simple need: capturing ProjektMelody's streams on Chaturbate. Chaturbate doesn’t save VODs, and sometimes we missed the magic. Other times, creators like ProjektMelody faced unnecessary de-platforming for simply being unique. We wanted to create a space where this content could endure, unshaken by the tides of censorship or fleeting platforms.
diff --git a/apps/bright/lib/bright_web/controllers/page_html/api.html.heex b/apps/bright/lib/bright_web/controllers/page_html/api.html.heex
index 218cd61..052c731 100644
--- a/apps/bright/lib/bright_web/controllers/page_html/api.html.heex
+++ b/apps/bright/lib/bright_web/controllers/page_html/api.html.heex
@@ -6,5 +6,18 @@
@todo @todo documentation
icons test
+ <.icon name="bittorrent" type="solid"/> + <.icon name="hammer" type="solid"/> + <.icon name="fansly" type="solid"/> + <.icon name="reddit" type="solid"/> + <.icon name="chaturbate" type="solid"/> + <.icon name="throne" type="solid"/> + <.icon name="tiktok" type="solid"/> + <.icon name="pornhub" type="solid"/> +- <.icon name="circle-exclamation" class="icon h-4 w-4" /> + <% # <.icon name="circle-exclamation" class="icon h-4 w-4" /> %> + (Yeah, we’re still testing.) check back each week for updates.
diff --git a/apps/bright/lib/bright_web/controllers/stream_html/index.html.heex b/apps/bright/lib/bright_web/controllers/stream_html/index.html.heex index 59143ed..aa48eb7 100644 --- a/apps/bright/lib/bright_web/controllers/stream_html/index.html.heex +++ b/apps/bright/lib/bright_web/controllers/stream_html/index.html.heex @@ -15,7 +15,8 @@