lightnet 3.11.0 → 3.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # lightnet
2
2
 
3
+ ## 3.12.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#351](https://github.com/LightNetDev/LightNet/pull/351) [`a1f63bf`](https://github.com/LightNetDev/LightNet/commit/a1f63bfa30448fdf58bfb1ff24007caa750349e0) Thanks [@smn-cds](https://github.com/smn-cds)! - Add an external fallback UI for audio content so non-mp3 URLs show a player-style link that opens in a new tab.
8
+
9
+ - [#351](https://github.com/LightNetDev/LightNet/pull/351) [`a1f63bf`](https://github.com/LightNetDev/LightNet/commit/a1f63bfa30448fdf58bfb1ff24007caa750349e0) Thanks [@smn-cds](https://github.com/smn-cds)! - Add a fallback in the video player so any video URL can be used on the "video" details page. Unsupported providers now render a poster-style link that opens the video on the external site.
10
+
3
11
  ## 3.11.0
4
12
 
5
13
  ### Minor Changes
@@ -10,9 +10,9 @@ case `uname` in
10
10
  esac
11
11
 
12
12
  if [ -z "$NODE_PATH" ]; then
13
- export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.16.6_@types+node@25.0.3_jiti@2.4.2_lightningcss@1.29.1_rollup@4.54.0_terser@5.39.0_typescript@5.9.3_yaml@2.8.2/node_modules/astro/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.16.6_@types+node@25.0.3_jiti@2.4.2_lightningcss@1.29.1_rollup@4.54.0_terser@5.39.0_typescript@5.9.3_yaml@2.8.2/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules"
13
+ export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.16.6_@types+node@25.0.3_jiti@2.4.2_lightningcss@1.29.1_rollup@4.55.1_terser@5.39.0_typescript@5.9.3_yaml@2.8.2/node_modules/astro/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.16.6_@types+node@25.0.3_jiti@2.4.2_lightningcss@1.29.1_rollup@4.55.1_terser@5.39.0_typescript@5.9.3_yaml@2.8.2/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules"
14
14
  else
15
- export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.16.6_@types+node@25.0.3_jiti@2.4.2_lightningcss@1.29.1_rollup@4.54.0_terser@5.39.0_typescript@5.9.3_yaml@2.8.2/node_modules/astro/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.16.6_@types+node@25.0.3_jiti@2.4.2_lightningcss@1.29.1_rollup@4.54.0_terser@5.39.0_typescript@5.9.3_yaml@2.8.2/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules:$NODE_PATH"
15
+ export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.16.6_@types+node@25.0.3_jiti@2.4.2_lightningcss@1.29.1_rollup@4.55.1_terser@5.39.0_typescript@5.9.3_yaml@2.8.2/node_modules/astro/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.16.6_@types+node@25.0.3_jiti@2.4.2_lightningcss@1.29.1_rollup@4.55.1_terser@5.39.0_typescript@5.9.3_yaml@2.8.2/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules:$NODE_PATH"
16
16
  fi
17
17
  if [ -x "$basedir/node" ]; then
18
18
  exec "$basedir/node" "$basedir/../astro/astro.js" "$@"
@@ -8,7 +8,7 @@
8
8
  "@astrojs/tailwind": "^6.0.2",
9
9
  "@lightnet/decap-admin": "^3.1.4",
10
10
  "astro": "^5.16.6",
11
- "lightnet": "^3.11.0",
11
+ "lightnet": "^3.12.0",
12
12
  "react": "^19.2.3",
13
13
  "react-dom": "^19.2.3",
14
14
  "sharp": "^0.34.5",
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "LightNet makes it easy to run your own digital media library.",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
- "version": "3.11.0",
6
+ "version": "3.12.0",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "https://github.com/LightNetDev/lightnet",
@@ -50,16 +50,16 @@
50
50
  "@hookform/resolvers": "^5.2.2",
51
51
  "@iconify-json/mdi": "^1.2.3",
52
52
  "@iconify/tailwind": "^1.2.0",
53
- "@mdxeditor/editor": "^3.52.1",
53
+ "@mdxeditor/editor": "^3.52.3",
54
54
  "@tailwindcss/typography": "^0.5.19",
55
- "@tanstack/react-virtual": "^3.13.13",
55
+ "@tanstack/react-virtual": "^3.13.17",
56
56
  "daisyui": "^4.12.24",
57
57
  "embla-carousel": "^8.6.0",
58
58
  "embla-carousel-wheel-gestures": "^8.1.0",
59
59
  "fuse.js": "^7.1.0",
60
60
  "i18next": "^25.7.3",
61
61
  "marked": "^16.4.2",
62
- "react-hook-form": "^7.69.0",
62
+ "react-hook-form": "^7.70.0",
63
63
  "yaml": "^2.8.2"
64
64
  },
65
65
  "devDependencies": {
@@ -6,6 +6,8 @@ import {
6
6
  type CodeBlockEditorProps,
7
7
  codeBlockPlugin,
8
8
  CreateLink,
9
+ diffSourcePlugin,
10
+ DiffSourceToggleWrapper,
9
11
  headingsPlugin,
10
12
  linkDialogPlugin,
11
13
  linkPlugin,
@@ -22,6 +24,7 @@ import {
22
24
  type Control,
23
25
  Controller,
24
26
  type FieldValues,
27
+ get,
25
28
  type Path,
26
29
  } from "react-hook-form"
27
30
 
@@ -42,7 +45,10 @@ export default function LazyLoadedMarkdownEditor<
42
45
  <Controller
43
46
  control={control}
44
47
  name={name}
45
- render={({ field: { onBlur, onChange, value, ref } }) => (
48
+ render={({
49
+ field: { onBlur, onChange, value, ref },
50
+ formState: { defaultValues },
51
+ }) => (
46
52
  <MDXEditor
47
53
  markdown={value ?? ""}
48
54
  onBlur={onBlur}
@@ -68,15 +74,16 @@ export default function LazyLoadedMarkdownEditor<
68
74
  linkDialogPlugin(),
69
75
  quotePlugin(),
70
76
  markdownShortcutPlugin(),
77
+ diffSourcePlugin({ diffMarkdown: get(defaultValues, name) }),
71
78
  toolbarPlugin({
72
79
  toolbarContents: () => (
73
- <>
80
+ <DiffSourceToggleWrapper>
74
81
  <UndoRedo />
75
82
  <BoldItalicUnderlineToggles />
76
83
  <BlockTypeSelect />
77
84
  <ListsToggle options={["bullet", "number"]} />
78
85
  <CreateLink />
79
- </>
86
+ </DiffSourceToggleWrapper>
80
87
  ),
81
88
  toolbarClassName: "!rounded-none !bg-slate-200",
82
89
  }),
@@ -22,9 +22,9 @@ export default function Label({
22
22
  return "bg-rose-800 text-white"
23
23
  }
24
24
  if (isDirty) {
25
- return "bg-slate-400 text-slate-950"
25
+ return "bg-slate-600 text-slate-50"
26
26
  }
27
- return "bg-slate-300 text-slate-800"
27
+ return "bg-slate-300 text-slate-700"
28
28
  }
29
29
  return (
30
30
  <div
@@ -16,7 +16,7 @@ export const getBorderClass = ({
16
16
  return "border border-rose-800 " + focusColors(focusWithin)
17
17
  }
18
18
  if (isDirty) {
19
- return "border border-slate-400 " + focusColors(focusWithin)
19
+ return "border border-slate-600 " + focusColors(focusWithin)
20
20
  }
21
21
  return "border border-slate-300 " + focusColors(focusWithin)
22
22
  }
@@ -42,8 +42,8 @@ export default function Content({
42
42
  return <span></span>
43
43
  }
44
44
  return (
45
- <span className="ms-1 flex items-center gap-1 rounded-lg bg-sky-700 px-2 py-1 text-xs uppercase text-slate-100">
46
- <Icon className="text-sm mdi--star" ariaLabel="" />
45
+ <span className="ms-1 flex items-center gap-1 rounded-lg bg-slate-200 px-2 py-1 text-xs font-bold uppercase text-slate-600">
46
+ <Icon className="text-sm text-sky-700 mdi--star" ariaLabel="" />
47
47
  {t("ln.admin.primary-content")}
48
48
  </span>
49
49
  )
@@ -1,6 +1,9 @@
1
1
  ---
2
2
  import type { ImageMetadata } from "astro"
3
3
  import { getImage } from "astro:assets"
4
+ import { Image } from "astro:assets"
5
+
6
+ import Icon from "./Icon"
4
7
 
5
8
  interface Props {
6
9
  /**
@@ -12,21 +15,43 @@ interface Props {
12
15
  */
13
16
  title?: string
14
17
  /**
15
- * Poster image to use for the mp4 video player.
18
+ * Poster image to use for the mp4 video player or external fallback.
16
19
  */
17
20
  image?: ImageMetadata
18
21
  }
19
22
 
20
23
  const { url, title, image: imageMetadata } = Astro.props
21
24
 
22
- const { host, id, image } = await parseUrl(url)
25
+ const { host, id, image, href } = await parseUrl(url, imageMetadata)
26
+ const { t } = Astro.locals.i18n
23
27
 
24
- async function parseUrl(urlToParse: string): Promise<{
25
- host: "youtube" | "vimeo" | "mp4"
28
+ async function parseUrl(
29
+ urlToParse: string,
30
+ imageMetadata?: ImageMetadata,
31
+ ): Promise<{
32
+ host: "youtube" | "vimeo" | "mp4" | "external"
26
33
  id: string | null
27
34
  image?: string
35
+ href?: string
28
36
  }> {
29
- const url = new URL(urlToParse)
37
+ const resolvedImage = async () => {
38
+ if (!imageMetadata) {
39
+ return undefined
40
+ }
41
+ return (await getImage({ src: imageMetadata, format: "webp" })).src
42
+ }
43
+
44
+ let url: URL
45
+ try {
46
+ url = new URL(urlToParse)
47
+ } catch {
48
+ return {
49
+ host: "external",
50
+ id: null,
51
+ href: urlToParse,
52
+ image: await resolvedImage(),
53
+ }
54
+ }
30
55
  // https://www.youtube.com/embed/ABC123abc
31
56
  // https://www.youtube.com/watch?v=ABC123abc
32
57
  if (url.hostname === "www.youtube.com") {
@@ -54,12 +79,15 @@ async function parseUrl(urlToParse: string): Promise<{
54
79
 
55
80
  // https://domain.com/video.mp4
56
81
  if (url.pathname.endsWith(".mp4")) {
57
- const image =
58
- imageMetadata &&
59
- (await getImage({ src: imageMetadata, format: "webp" })).src
82
+ const image = await resolvedImage()
60
83
  return { host: "mp4", id: url.toString(), image }
61
84
  }
62
- throw Error(`Unsupported video url: ${urlToParse}`)
85
+ return {
86
+ host: "external",
87
+ id: null,
88
+ href: url.toString(),
89
+ image: await resolvedImage(),
90
+ }
63
91
  }
64
92
  ---
65
93
 
@@ -86,6 +114,42 @@ async function parseUrl(urlToParse: string): Promise<{
86
114
  <video class="h-full w-full" controls preload="auto" poster={image}>
87
115
  <source src={id} type="video/mp4" />
88
116
  </video>
89
- ) : null
117
+ ) : (
118
+ <a
119
+ class="group relative flex h-full w-full items-center justify-center focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80 focus-visible:ring-offset-2 focus-visible:ring-offset-black"
120
+ href={href ?? url}
121
+ target="_blank"
122
+ rel="noreferrer noopener"
123
+ >
124
+ {imageMetadata ? (
125
+ <Image
126
+ src={imageMetadata}
127
+ alt={title ?? ""}
128
+ class="h-full w-full object-cover"
129
+ />
130
+ ) : (
131
+ <div class="flex h-full w-full items-center justify-center bg-gray-900 text-gray-200">
132
+ <span class="text-sm font-medium">{t("ln.external-link")}</span>
133
+ </div>
134
+ )}
135
+ <div class="absolute inset-0 z-10 bg-black/35 transition group-hover:bg-black/50" />
136
+ <div class="absolute inset-0 z-20 flex items-center justify-center">
137
+ <span class="flex h-14 w-14 items-center justify-center rounded-full bg-white/90 text-black shadow-lg transition group-hover:scale-105">
138
+ <svg
139
+ class="h-7 w-7"
140
+ viewBox="0 0 24 24"
141
+ fill="currentColor"
142
+ aria-hidden="true"
143
+ >
144
+ <path d="M8 5v14l11-7z" />
145
+ </svg>
146
+ </span>
147
+ </div>
148
+ <div class="absolute bottom-0 end-0 start-0 z-20 flex items-center gap-2 rounded bg-black/70 px-4 py-4 text-sm font-medium text-white backdrop-blur md:text-sm">
149
+ <Icon className="mdi--external-link" ariaLabel="" />
150
+ {t("ln.external-link")}
151
+ </div>
152
+ </a>
153
+ )
90
154
  }
91
155
  </div>
@@ -60,12 +60,11 @@ ln.previous: Previous
60
60
  # English: Next
61
61
  ln.next: Next
62
62
 
63
- # Accessibility label for an external link.
64
- # This is only "visible" to a screen-reader.
63
+ # Hint for an external link.
65
64
  #
66
- # English: External link
65
+ # English: External link - opens in a new tab
67
66
  # Used on: https://sk8-ministries.pages.dev/en/media/ollie-with-integrity--en/
68
- ln.external-link: External link
67
+ ln.external-link: External link - opens in a new tab
69
68
 
70
69
  # Title for the search page.
71
70
  #
@@ -1,6 +1,4 @@
1
1
  ---
2
- import { AstroError } from "astro/errors"
3
-
4
2
  import { getMediaItem } from "../../../content/get-media-items"
5
3
  import { createContentMetadata } from "../utils/create-content-metadata"
6
4
  import AudioPlayer from "./AudioPlayer.astro"
@@ -15,16 +13,7 @@ const { t } = Astro.locals.i18n
15
13
 
16
14
  const item = await getMediaItem(mediaId)
17
15
 
18
- const content = item.data.content
19
- .map((c) => createContentMetadata(c))
20
- .filter((c) => c.extension === "mp3")
21
-
22
- if (!content.length) {
23
- throw new AstroError(
24
- `Missing mp3 content for ${mediaId}`,
25
- `Add at least one mp3 file to the content array of /src/media/${mediaId}.json`,
26
- )
27
- }
16
+ const content = item.data.content.map((c) => createContentMetadata(c))
28
17
  ---
29
18
 
30
19
  <ol
@@ -1,20 +1,52 @@
1
1
  ---
2
- import { AstroError } from "astro/errors"
2
+ import Icon from "../../../components/Icon"
3
3
 
4
4
  type Props = {
5
5
  src: string
6
6
  className?: string
7
7
  }
8
8
  const { src, className } = Astro.props
9
- if (!src.toLowerCase().endsWith(".mp3")) {
10
- throw new AstroError(
11
- `Unsupported audio file ${src}`,
12
- "To fix the issue, reference a mp3 file.",
13
- )
14
- }
9
+ const { t } = Astro.locals.i18n
10
+
11
+ const normalizedSrc = src.trim()
12
+ const audioExtension = normalizedSrc.split("?")[0].split("#")[0].toLowerCase()
13
+ const isMp3 = audioExtension.endsWith(".mp3")
15
14
  ---
16
15
 
17
- <audio class="rounded-2xl" class:list={[className]} src={src} controls></audio>
16
+ {
17
+ isMp3 ? (
18
+ <audio class="rounded-2xl" class:list={[className]} src={src} controls />
19
+ ) : (
20
+ <a
21
+ class="group flex w-full items-center gap-4 rounded-2xl border border-gray-200 bg-white px-4 py-4 shadow-sm transition hover:border-gray-300 hover:bg-gray-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-gray-900/70"
22
+ class:list={[className]}
23
+ href={src}
24
+ target="_blank"
25
+ rel="noreferrer noopener"
26
+ aria-label={t("ln.external-link")}
27
+ >
28
+ <span class="flex h-12 w-12 items-center justify-center rounded-full border border-gray-700 text-gray-700 shadow-sm transition group-hover:scale-105">
29
+ <svg
30
+ class="h-6 w-6"
31
+ viewBox="0 0 24 24"
32
+ fill="currentColor"
33
+ aria-hidden="true"
34
+ >
35
+ <path d="M8 5v14l11-7z" />
36
+ </svg>
37
+ </span>
38
+ <div class="flex h-full grow flex-col gap-3">
39
+ <span class="flex items-center gap-2 font-semibold text-gray-900">
40
+ <Icon className="mdi--external-link" ariaLabel="" />
41
+ {t("ln.external-link")}
42
+ </span>
43
+ <div class="h-2 w-full rounded-full bg-gray-200">
44
+ <div class="h-full w-2 rounded-full bg-gray-400" />
45
+ </div>
46
+ </div>
47
+ </a>
48
+ )
49
+ }
18
50
 
19
51
  <style>
20
52
  audio::-webkit-media-controls-enclosure {