为特定用户个人资料添加背景视频?

我目前正尝试将视频添加到特定的用户个人资料页面,以便我们所有的赞助者都能在他们的个人资料上拥有特定的背景视频。(在你大惊小怪之前,这只是一个循环动画,而不是一个完整的视频——看起来应该很不错,类似于 Steam 个人资料背景。)

以下 HTML 和 CSS 代码适用于所有用户——但这显然不是我们想要的:

// 这部分放在“Header”选项卡中

<video playsinline autoplay muted loop id="myVideo" poster="[INSERT LINK]">
	<source src="[INSERT LINK]" type="video/webm">
	<source src="[INSERT LINK]" type="video/mp4">
</video>
#myVideo {
  position: fixed;
  top: 63px;
  min-height: 1080px;
  margin-left: 50vw;
  transform: translate(-50%);
}

.user-content
{
    background: none;
}

.user-main .about.has-background .details {
    padding-bottom: 15px;
}
.user-main .about
{
    margin-bottom: 0px;
}
.user-content-wrapper
{
    background: rgba(var(--secondary-rgb), 0.8);
}

与使用 body.category-general 将图像添加到“general”类别中的页面不同,似乎没有为特定组或特定用户名的用户分配个人资料页面。我们对这方面了解不多,主要有 CSS 经验,而不是直接处理 HTML,因此不确定是否有简单/方便的方法可以实现我们想要的效果。

我们设想的最佳方法是根据用户组为用户个人资料添加类似的 slug,但我们不确定如何实现这一点以及如何仅在具有正确内容的页面上显示视频,并且我们也不打算仅使用此方法,如果存在其他更简单的方法。

例如,如果更容易,我们也愿意考虑逐个用户而不是逐个组进行此操作。

我们只是希望不要在每个页面上都硬编码视频,以便它仅在您查看特定用户时加载。

编辑: 我应该提到我们使用的是稳定分支,以防万一这会改变任何事情。

我们目前的方法是查看我们是否可以通过规范链接检测到我们是否在某个用户的页面上,如果是,则应用视频。因此,我们有以下内容:

<script type="text/discourse-plugin" version="0.8">
    api.onPageChange(() => {
        determineUser();
    });
    
    function determineUser() {
        var pageURL = document.querySelector("link[rel='canonical']").getAttribute("href");
        var isUserPage = pageURL.includes("https://www.fortressoflies.com/u/");
        document.documentElement.style.setProperty('--currUsername', pageURL);
        if(isUserPage)
        {
            document.documentElement.style.setProperty('--lastUsername', pageURL);
            $('body').css('background-color', '#'+(Math.random()*0xFFFFFF<<0).toString(16));
        }
    }
</script>

但是,这似乎只在完全刷新时才有效 - 不知何故,从一页点击到另一页不会更新 --currUsername 属性,并且用户页面的背景色不是随机应用,而是如果最后一次按 F5 键是在用户页面上,则所有页面的背景色都是随机应用的,而如果最后一次按 F5 键是在非用户页面上,则任何页面都没有应用。

坦白说,我对 JavaScript 没有足够的经验,不知道为什么会这样——在我看来,在页面更改时,函数应该触发(它确实触发了),导致 pageURL 变量被更新,这应该会在加载页面时导致 --currUsername 属性被更新。但是,这只在完全刷新时发生,否则变量似乎不会改变。

有什么想法知道我在这里搞砸了什么吗?

这似乎是因为规范 URL 未更新,而“og:url”属性已更新。

唯一的问题是使用 var pageURL = document.querySelector("meta[property='og:url']").getAttribute("content"); 会在 meta 标签本身更新之前更新——也就是说,这段代码获取的是前一个页面的 URL,而不是当前页面的 URL。

如果你想在特定页面上添加内容,最佳选择是使用 plugin-outlet(插件插槽)。简而言之,plugin-outlet 是 Discourse 模板中预留的空间,你可以利用它们来添加新内容。

你需要做的第一件事是确认目标页面上是否存在 plugin-outlet。有一个主题组件可以帮你完成这项工作。

安装该组件后,将其启用,然后前往你想要目标化的页面,检查有哪些可用的插槽。在你的案例中,确实存在这样一个 plugin-outlet(已用绿色高亮显示)

所以,我们需要的是 above-user-profile

假设它不存在……那该怎么办?在这种情况下,最佳选择是请求添加它,或者提交 PR 将其添加到核心代码中。如果你的使用场景合理,大多数情况下都会被接受。

无论如何,正如我之前所说,在这种情况下它已经存在了。那么,让我们看看如何向其中添加标记。对于接下来的步骤,你不再需要上面的组件,既然你已经知道了插件插槽的名称,现在可以禁用它了。

你只需要在主题的“Header(头部)”选项卡中添加类似以下内容:

<script type="text/x-handlebars" data-template-name="/connectors/OUTLET_NAME/SOME_NAME">
  Your markup goes here...
</script>

你需要将 OUTLET_NAME 更改为你想要目标化的插槽名称。然后将 SOME_NAME 更改为你想为此自定义功能起的名称。名称可以是任何内容,但如果可能的话,请尽量具有描述性。这是一种良好的实践。因此,我们最终得到以下内容:

<script type="text/x-handlebars" data-template-name="/connectors/above-user-profile/add-profile-videos">
  Your markup goes here...
  <h1>Hello World!!</h1>
</script>

让我们试试这个,看看会发生什么……请记住,上面的代码片段应放在你主题的 common > header 选项卡中。

然后……

到目前为止一切顺利,但让我们深入探究一下。

你并不希望视频在所有个人资料页面上显示,而是希望仅根据某些条件显示。那么,该怎么做呢?你需要两样东西:一些要消费的数据和一点 JavaScript 代码。

让我们先找到数据。还记得我说过 plugin-outlet 是预留空间吗?如果没有上下文,保留它们有什么意义?这就是为什么 Discourse 会将相关的上下文信息传递给每个 plugin-outlet……但首先,让我们退一步。当你添加这段代码时:

<script type="text/x-handlebars" data-template-name="/connectors/above-user-profile/add-profile-videos">
  Your markup goes here...
  <h1>Hello World!!</h1>
</script>

它看起来像 HTML——script 标签确实是——但标签内部的内容被视为 Handlebars 代码。

这意味着你可以这样做:

<script type="text/x-handlebars" data-template-name="/connectors/above-user-profile/add-profile-videos">
  {{log this}}
</script>

然后检查浏览器控制台。每当插槽被渲染时(即当你位于用户页面时),你都会看到以下内容:

现在,这些信息有帮助吗?是的……但目前还不行。我们稍后会回到这个话题。让我们再退一步,看看 Discourse 如何将上下文传递给插槽。如果你在 Github 上(或在本地)搜索插槽名称,会得到以下结果:

让我们打开该文件。你看到的第一行是:

看看该行的最后一部分:

args=(hash model=model)

你会看到 Discourse 将 model 作为参数传递给插槽。为了所有实际目的并保持简单,model = data(数据)。

因此,我们插槽的参数之一是 model,这就是我们所需数据所在的位置。所以,让我们回到我们的代码片段:

<script type="text/x-handlebars" data-template-name="/connectors/above-user-profile/add-profile-videos">
  {{log this}}
</script>

将其更改为:

<script type="text/x-handlebars" data-template-name="/connectors/above-user-profile/add-profile-videos">
-  {{log this}}
+  {{log args}}
</script>

现在我们在控制台中看到了以下内容:

你可以浏览这些数据,看看是否包含你需要的内容。应该包含,因为它包含了该页面上其他元素使用的关于用户的所有数据。这是该特定用户的用户页面的“模型”。

其中一个可用的属性是……鼓声 :drum: ……用户所属的群组。

所以,如果你执行:

{{log args.model.groups}}

你会在控制台中看到该用户所属的所有群组。

好的,现在我们有了所需的数据,剩下的就是基于此添加一些条件。

你可能会想,我们可以在同一个代码片段中完成这一点,但不幸的是,我们无法做到。Handlebars 是一种模板语言。它对逻辑的支持非常、非常基础——仅限于简单的真/假条件和循环。你无法进行比较和其他类似操作。

那么确切在哪里可以做呢?在连接器类(connector class)中,听起来很高级……我知道。

简而言之,连接器类本质上是一段附加到插槽的 JavaScript 代码。实际上它要复杂得多,但你现在只需要知道这些就够了。

那么,让我们创建一个。我们这样做:

<script type="text/discourse-plugin" version="0.8">
api.registerConnectorClass('OUTLET_NAME', 'SOME_NAME', {

});
</script>

这里的 OUTLET_NAMESOME_NAME 应该与上面使用的名称相同。所以让我们更改它们:

<script type="text/discourse-plugin" version="0.8">
api.registerConnectorClass('above-user-profile', 'add-profile-videos', {

});
</script>

此代码片段也应放在你主题的 common > header 选项卡中。所以你现在应该拥有类似以下内容:

<script type="text/x-handlebars" data-template-name="/connectors/above-user-profile/add-profile-videos">
  {{log args.model.groups}}
</script>

<script type="text/discourse-plugin" version="0.8">
  api.registerConnectorClass('above-user-profile', 'add-profile-videos', {

  });
</script>

在我们的连接器类内部,我们可以做一些工作……但是……我们需要记住,它并不像任何普通的 JavaScript 文件。由于缺乏更好的描述……把它想象成一个精简版的 Ember 组件。进一步展开这个话题超出了本文的范围,所以我们继续。

默认情况下,有四个方法与其连接:

actions 允许你定义操作,如下所示:

api.registerConnectorClass("above-user-profile", "add-profile-videos", {
  actions: {
    myAction() {
      // do something
    }
  }
});

然后你可以在插槽内部调用该操作,例如当按钮被按下时。我们这里不需要这个,所以让我们继续。

api.registerConnectorClass("above-user-profile", "add-profile-videos", {
  shouldRender(args, component) {
    // return true or false here
  }
});

我们也不会使用这个,因为插槽仅在个人资料页面上渲染,而且我们目前没有其他要求。不过,你可以使用它在插槽渲染之前添加任何你想要测试的条件。例如,当前用户的信任级别等。继续……

api.registerConnectorClass("above-user-profile", "add-profile-videos", {
  setupComponent(args, component) {
    // do something
  }
});

这是我们要关注的重点。任何你想要设置的 JavaScript 条件或变量都放在这里。在深入探讨这个之前,让我们先涵盖最后一个方法,以求完整:

api.registerConnectorClass("above-user-profile", "add-profile-videos", {
  teardownComponent(args, component) {
    // do something
  }
});

当插槽即将被移除时会触发此方法。因此,它允许你执行任何所需的清理工作,例如移除事件监听器等。

好的,让我们回到 setupComponent

api.registerConnectorClass("above-user-profile", "add-profile-videos", {
  setupComponent(args, component) {
    // do something
  }
});

你可以看到它接收了两个参数。首先是 args,然后是 component

这里的 args 与我们之前查看的内容相同。它是 Discourse 传递给插槽的上下文数据。所以,如果你执行:

api.registerConnectorClass("above-user-profile", "add-profile-videos", {
  setupComponent(args, component) {
    console.log(args.model.groups);
  }
});

你会在浏览器控制台中看到与我们之前看到相同的信息。即个人资料所有者所属的群组。有趣的部分开始了,你现在拥有了数据,并且有了正确的钩子。因此,你可以在这里做任何你想做的事。所以,如果我希望视频仅在属于特定群组的成员个人资料上显示,我可以这样做:

  api.registerConnectorClass('above-user-profile', 'add-profile-videos', {
    setupComponent(args, component) {
      const inGroup = [...args.model.groups].filter(g => g.name === TARGET_GROUP)
      const showVideo = inGroup.length ? true : false;

      console.log(showVideo);
    }
  });

如果你在属于 staff 群组的用户的个人资料页面上尝试此操作,它将在控制台中打印 true。所以,现在我们要做的最后一件事是将该值传递给插槽模板。以下是操作方法:

传递给 setupComponentcomponent 在连接器和插槽之间是共享的。你可以通过将其作为属性设置在组件上来向插槽传递内容,如下所示:

  const TARGET_GROUP = "staff"

  api.registerConnectorClass('above-user-profile', 'add-profile-videos', {
    setupComponent(args, component) {
      const inGroup = [...args.model.groups].filter(g => g.name === TARGET_GROUP)
      const showVideo = inGroup.length ? true : false;
-     console.log(showVideo);
+     component.setProperties({showVideo})
    }
  });

现在,如果我们回到模板并执行类似以下操作:

{{log showVideo}}

它将打印相同的结果。因此,我们现在将其放入 Handlebars 条件中,并在其中添加你的标记,如下所示:

<script
  type="text/x-handlebars"
  data-template-name="/connectors/above-user-profile/add-profile-videos"
>
  {{#if showVideo}}
    <video playsinline autoplay muted loop id="myVideo" poster="[INSERT LINK]">
      <source src="[INSERT LINK]" type="video/webm">
      <source src="[INSERT LINK]" type="video/mp4">
    </video>
  {{/if}}
</script>

然后检查 staff 用户的个人资料页面。你会看到视频已加载。

一旦你离开该 staff 成员的个人资料页面,视频就会消失。视频不会显示在非 staff 群组用户的个人资料上。

所以,让我们把所有这些整合起来。这与上面的内容相同。

以下是我使用的 CSS。位于 common > css 选项卡中:

#myVideo {
  position: fixed;
  top: var(--header-offset);
  min-height: 100vh;
  left: 0;
  z-index: -1;
}

.user-content {
  background: none;
}

.user-main {
  padding: 0.5em;
  background: rgba(var(--secondary-rgb), 0.8);
}

// 如果你也想在移动端显示
.mobile-view {
  body[class*="user-"] {
    background: none;
    .user-main,
    .user-content {
      padding: 0.5em;
      background: rgba(var(--secondary-rgb), 0.8);
    }
  }
}

HTML / JavaScript / Handlebars。这应放在你主题的 common > header 选项卡中:

<script
  type="text/x-handlebars"
  data-template-name="/connectors/above-user-profile/add-profile-videos"
>
  {{#if showVideo}}
    <video playsinline autoplay muted loop id="myVideo" poster="[INSERT LINK]">
      <source src="[INSERT LINK]" type="video/webm">
      <source src="[INSERT LINK]" type="video/mp4">
    </video>
  {{/if}}
</script>

<script type="text/discourse-plugin" version="0.8">
  const TARGET_GROUP = "staff"

  api.registerConnectorClass('above-user-profile', 'add-profile-videos', {
    setupComponent(args, component) {
      const inGroup = [...args.model.groups].filter(g => g.name === TARGET_GROUP)
      const showVideo = inGroup.length ? true : false;
      component.setProperties({showVideo})
    }
  });
</script>

TARGET_GROUP 更改为你想要目标化的群组名称,并添加视频的 src 属性。

这篇帖子有点长……不要被吓倒。一旦你掌握了概念,上面我们做的所有事情都可以在 3-5 分钟内完成。

这里的好处是,我们讨论的所有内容对于任何 plugin-outlet 都基本相同。唯一变化的是名称。因此,这适用于你未来想要进行的任何 plugin-outlet 修改。

  1. 找到插槽名称
  2. 获取数据
  3. 在连接器中处理数据
  4. 将属性传回模板

这非常深入,我下周有时间时一定会仔细看看,但 suffice to say 从快速浏览来看,它似乎比我目前的实现好得多(在每个页面上嵌入视频,并且只在用户个人资料中显示,我通过一个脚本实现的,该脚本会在用户的帐户名称是某个特定名称时向用户页面的 body 添加一个标签)。感谢您深入的解释,我迫不及待地想开始动手了!

好的,总的来说,这种方法效果很好,解释也很棒。非常感谢您的额外付出。

在用户基础上工作很简单——我们只需像这样检查给定的用户名:

      const isUser1 = args.model.username == "User1"
      component.setProperties({isUser1})

但是,我们在 CSS 方面遇到一个问题——我们希望仅针对这些用户更改用户页面的外观。

目前,我们通过与之前相同的代码来实现这一点:

<script type="text/discourse-plugin" version="0.8">
    api.onPageChange(() =>{
        window.onload = determineUser();
    });
    
    async function determineUser() {
        await sleep(50);
        var pageURL = document.querySelector("meta[property='og:url']").getAttribute("content");
        var isUserPage = pageURL.includes("https://www.siteurl.com/u/");
        var isUser1 = pageURL.includes("u/User1/");
        document.body.className = document.body.className.replace(" user-page-animated","");

        
        if(isUserPage)
        {
            if(isUser1)
            {
                document.body.className += ' user-page-animated';
            }
        }
    }
    
    function sleep(ms)
    {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
</script>

这使我们能够为每个新用户简单地复制粘贴“User1”代码,但依赖于每次页面加载后 50 毫秒的延迟才能触发,这会显示给最终用户(如果删除,则因某种原因不起作用)。

是否有任何方法可以将此向 body 添加类的功能也集成到您提供的代码中,以便我们可以使用它来以不同于没有视频的页面样式化带有视频的页面?

而且,再次感谢您如此详尽的解释。