瀏覽代碼

feat: more auto-like stuff

Sv443 11 月之前
父節點
當前提交
bbd9ba9cef
共有 7 個文件被更改,包括 129 次插入44 次删除
  1. 58 14
      src/dialogs/autoLikeChannels.ts
  2. 1 1
      src/features/index.ts
  3. 18 0
      src/features/input.css
  4. 0 1
      src/features/input.ts
  5. 7 2
      src/features/layout.css
  6. 32 22
      src/interface.ts
  7. 13 4
      src/types.ts

+ 58 - 14
src/dialogs/autoLikeChannels.ts

@@ -1,4 +1,4 @@
-import { getDomain, t } from "../utils";
+import { getDomain, onInteraction, t } from "../utils";
 import { BytmDialog, createCircularBtn, createToggleInput } from "../components";
 import { autoLikeChannelsStore } from "../features";
 import { debounce } from "@sv443-network/userutils";
@@ -6,12 +6,13 @@ import { debounce } from "@sv443-network/userutils";
 let autoLikeChannelsDialog: BytmDialog | null = null;
 
 /** Creates and/or returns the import dialog */
-export function getAutoLikeChannelsDialog() {
+export async function getAutoLikeChannelsDialog() {
   if(!autoLikeChannelsDialog) {
+    await autoLikeChannelsStore.loadData();
     autoLikeChannelsDialog = new BytmDialog({
       id: "auto-like-channels",
-      width: 500,
-      height: 700,
+      width: 700,
+      height: 600,
       closeBtnEnabled: true,
       closeOnBgClick: true,
       closeOnEscPress: true,
@@ -42,26 +43,63 @@ async function renderBody() {
 
   contElem.appendChild(descriptionEl);
 
+  const addNewEl = document.createElement("div");
+  addNewEl.id = "bytm-auto-like-channels-add-new";
+  addNewEl.role = "button";
+  addNewEl.tabIndex = 0;
+  addNewEl.textContent = `+ ${t("add_new")}`;
+  addNewEl.classList.add("bytm-link");
+
+  onInteraction(addNewEl, async () => {
+    const id = prompt(t("add_auto_like_channel_id_prompt")); // TODO
+    if(!id)
+      return;
+
+    if(autoLikeChannelsStore.getData().channels.some((ch) => ch.id === id))
+      return alert(t("add_auto_like_channel_already_exists")); // TODO
+
+    const name = prompt(t("add_auto_like_channel_name_prompt")); // TODO
+    if(!name)
+      return;
+
+    await autoLikeChannelsStore.setData({
+      channels: [
+        ...autoLikeChannelsStore.getData().channels,
+        { id, name, enabled: true },
+      ],
+    });
+
+    const unsub = autoLikeChannelsDialog?.on("clear", async () => {
+      unsub?.();
+      await autoLikeChannelsDialog?.open();
+    });
+
+    autoLikeChannelsDialog?.unmount();
+  });
+
+  contElem.appendChild(addNewEl);
+
   const channelListCont = document.createElement("div");
   channelListCont.id = "bytm-auto-like-channels-list";
 
-  const removeChannel = (id: string) => debounce(
-    () => autoLikeChannelsStore.setData({
-      channels: autoLikeChannelsStore.getData().channels.filter((ch) => ch.id !== id),
-    }),
-    250,
-    "falling"
-  );
+  const removeChannel = (id: string) => autoLikeChannelsStore.setData({
+    channels: autoLikeChannelsStore.getData().channels.filter((ch) => ch.id !== id),
+  });
 
   const setChannelEnabled = (id: string, enabled: boolean) => debounce(
     () => autoLikeChannelsStore.setData({
-      channels: autoLikeChannelsStore.getData().channels.map((ch) => ch.id === id ? { ...ch, enabled } : ch),
+      channels: autoLikeChannelsStore.getData().channels
+        .map((ch) => ch.id === id ? { ...ch, enabled } : ch),
     }),
     250,
-    "falling"
+    "rising"
   );
 
-  for(const { name, id, enabled } of autoLikeChannelsStore.getData().channels) {
+  const sortedChannels = autoLikeChannelsStore
+    .getData().channels
+    .sort((a, b) => a.name.localeCompare(b.name));
+
+  for(const { name, id, enabled } of sortedChannels) {
     const rowElem = document.createElement("div");
     rowElem.classList.add("bytm-auto-like-channel-row");
 
@@ -71,6 +109,7 @@ async function renderBody() {
     const nameLabelEl = document.createElement("label");
     nameLabelEl.ariaLabel = nameLabelEl.title = name;
     nameLabelEl.htmlFor = `bytm-auto-like-channel-list-toggle-${id}`;
+    nameLabelEl.classList.add("bytm-auto-like-channel-name-label");
 
     const nameElem = document.createElement("a");
     nameElem.classList.add("bytm-auto-like-channel-name", "bytm-link");
@@ -80,7 +119,12 @@ async function renderBody() {
     nameElem.rel = "noopener noreferrer";
     nameElem.tabIndex = 0;
 
+    const idElem = document.createElement("span");
+    idElem.classList.add("bytm-auto-like-channel-id");
+    idElem.textContent = idElem.title = id;
+
     nameLabelEl.appendChild(nameElem);
+    nameLabelEl.appendChild(idElem);
 
     const toggleElem = await createToggleInput({
       id: `bytm-auto-like-channel-list-toggle-${id}`,

+ 1 - 1
src/features/index.ts

@@ -455,7 +455,7 @@ export const featInfo = {
   openAutoLikeChannelsDialog: {
     type: "button",
     category: "input",
-    click: () => getAutoLikeChannelsDialog().open(),
+    click: () => getAutoLikeChannelsDialog().then(d => d.open()),
   },
 
   //#region lyrics

+ 18 - 0
src/features/input.css

@@ -19,6 +19,11 @@
   margin-bottom: 15px;
 }
 
+#bytm-auto-like-channels-add-new {
+  padding: 4px 15px;
+  margin-top: 20px;
+}
+
 .bytm-auto-like-channel-row:hover {
   background-color: var(--bytm-menu-bg-highlight);
 }
@@ -27,3 +32,16 @@
   --toggle-height: 18px !important;
   --toggle-width: 36px !important;
 }
+
+.bytm-auto-like-channel-name-label {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.bytm-auto-like-channel-id {
+  cursor: text;
+  font-size: 1.1rem;
+  color: #aaa;
+  margin-left: 10px;
+}

+ 0 - 1
src/features/input.ts

@@ -174,7 +174,6 @@ export const autoLikeChannelsStore = new DataStore<{
 export async function initAutoLikeChannels() {
   try {
     canCompress = await compressionSupported();
-    await autoLikeChannelsStore.loadData();
     if(getDomain() === "ytm") {
       let timeout: NodeJS.Timeout;
       // TODO:FIXME: needs actual fix instead of timeout

+ 7 - 2
src/features/layout.css

@@ -5,6 +5,9 @@
 }
 
 .bytm-generic-btn {
+  --bytm-generic-btn-width: 36px;
+  --bytm-generic-btn-height: 36px;
+
   display: inline-flex;
   align-items: center;
   justify-content: center;
@@ -13,8 +16,10 @@
   cursor: pointer;
   margin-left: 8px;
 
-  width: 36px;
-  height: 36px;
+  min-width: var(--bytm-generic-btn-width);
+  max-width: var(--bytm-generic-btn-width);
+  min-height: var(--bytm-generic-btn-height);
+  max-height: var(--bytm-generic-btn-height);
 
   border: 1px solid transparent;
   border-radius: 100%;

+ 32 - 22
src/interface.ts

@@ -155,23 +155,23 @@ export function emitInterface<
   TDetail extends InterfaceEvents[TEvt],
 >(
   type: TEvt | `bytm:siteEvent:${keyof SiteEventsMap}`,
-  ...data: (TDetail extends undefined ? [undefined?] : [TDetail])
+  ...detail: (TDetail extends undefined ? [undefined?] : [TDetail])
 ) {
-  getUnsafeWindow().dispatchEvent(new CustomEvent(type, { detail: data?.[0] ?? undefined }));
+  getUnsafeWindow().dispatchEvent(new CustomEvent(type, { detail: detail?.[0] ?? undefined }));
   //@ts-ignore
-  emitOnPlugins(type, undefined, ...data);
-  log(`Emitted interface event '${type}'${data && data.length > 0 ? " with data:" : ""}`, ...data);
+  emitOnPlugins(type, undefined, ...detail);
+  log(`Emitted interface event '${type}'${detail && detail.length > 0 ? " with data:" : ""}`, ...detail);
 }
 
 //#region register plugins
 
-/** Plugins that are queued up for registration */
+/** Map of plugin ID and plugins that are queued up for registration */
 const pluginsQueued = new Map<string, PluginItem>();
 
-/** Registered plugins including their event listener instance */
+/** Map of plugin ID and all registered plugins */
 const pluginsRegistered = new Map<string, PluginItem>();
 
-/** Auth tokens for plugins that have been registered */
+/** Map of plugin ID to auth token for plugins that have been registered */
 const pluginTokens = new Map<string, string>();
 
 /** Initializes plugins that have been registered already. Needs to be run after `bytm:ready`! */
@@ -199,11 +199,13 @@ function getPluginKey(plugin: PluginDefResolvable) {
 
 /** Converts a PluginDef object (full definition) into a PluginInfo object (restricted definition) or undefined, if undefined is passed */
 function pluginDefToInfo(plugin?: PluginDef): PluginInfo | undefined {
-  return plugin && {
-    name: plugin.plugin.name,
-    namespace: plugin.plugin.namespace,
-    version: plugin.plugin.version,
-  };
+  return plugin
+    ? {
+      name: plugin.plugin.name,
+      namespace: plugin.plugin.namespace,
+      version: plugin.plugin.version,
+    }
+    : undefined;
 }
 
 /** Checks whether two plugins are the same, given their resolvable definition objects */
@@ -224,22 +226,22 @@ export function emitOnPlugins<TEvtKey extends keyof PluginEventMap>(
 
 /**
  * @private FOR INTERNAL USE ONLY!  
- * Returns the internal plugin def and events objects via its name and namespace, or undefined if it doesn't exist
+ * Returns the internal plugin def and events objects via its name and namespace, or undefined if it doesn't exist.
  */
-export function getPlugin(name: string, namespace: string): PluginItem | undefined
+export function getPlugin(pluginName: string, namespace: string): PluginItem | undefined
 /**
  * @private FOR INTERNAL USE ONLY!  
- * Returns the internal plugin def and events objects via resolvable definition, or undefined if it doesn't exist
+ * Returns the internal plugin def and events objects via resolvable definition, or undefined if it doesn't exist.
  */
-export function getPlugin(plugin: PluginDefResolvable): PluginItem | undefined
+export function getPlugin(pluginDef: PluginDefResolvable): PluginItem | undefined
 /**
  * @private FOR INTERNAL USE ONLY!  
- * Returns the internal plugin def and events objects via plugin ID (consisting of namespace and name), or undefined if it doesn't exist
+ * Returns the internal plugin def and events objects via plugin ID (consisting of namespace and name), or undefined if it doesn't exist.
  */
 export function getPlugin(pluginId: string): PluginItem | undefined
 /**
  * @private FOR INTERNAL USE ONLY!  
- * Returns the internal plugin def and events objects, or undefined if it doesn't exist
+ * Returns the internal plugin def and events objects, or undefined if it doesn't exist.
  */
 export function getPlugin(...args: [pluginDefOrNameOrId: PluginDefResolvable | string, namespace?: string]): PluginItem | undefined {
   return typeof args[0] === "string" && typeof args[1] === "undefined"
@@ -261,20 +263,28 @@ export function getPluginInfo(token: string | undefined, name: string, namespace
  * @public Intended for general use in plugins.
  */
 export function getPluginInfo(token: string | undefined, plugin: PluginDefResolvable): PluginInfo | undefined
+/**
+ * Returns info about a registered plugin on the BYTM interface by its ID (consisting of namespace and name), or undefined if the plugin isn't registered.  
+ * This is an authenticated function so you must pass the session- and plugin-unique token, retreived at registration.  
+ * @public Intended for general use in plugins.
+ */
+export function getPluginInfo(token: string | undefined, pluginId: string): PluginInfo | undefined
 /**
  * Returns info about a registered plugin on the BYTM interface, or undefined if the plugin isn't registered.  
  * This is an authenticated function so you must pass the session- and plugin-unique token, retreived at registration.  
  * @public Intended for general use in plugins.
  */
-export function getPluginInfo(...args: [token: string | undefined, pluginDefOrName: PluginDefResolvable | string, namespace?: string]): PluginInfo | undefined {
+export function getPluginInfo(...args: [token: string | undefined, pluginDefOrNameOrId: PluginDefResolvable | string, namespace?: string]): PluginInfo | undefined {
   if(resolveToken(args[0]) === undefined)
     return undefined;
 
   return pluginDefToInfo(
     pluginsRegistered.get(
-      args.length === 2
-        ? `${args[2]}/${args[1]}`
-        : getPluginKey(args[1] as PluginDefResolvable)
+      typeof args[1] === "string" && typeof args[2] === "undefined"
+        ? args[1]
+        : args.length === 2
+          ? `${args[2]}/${args[1]}`
+          : getPluginKey(args[1] as PluginDefResolvable)
     )?.def
   );
 }

+ 13 - 4
src/types.ts

@@ -85,7 +85,8 @@ declare global {
 
 /**
  * Intents (permissions) BYTM has to grant your plugin for it to be able to access certain features.  
- * TODO: this feature is unfinished, but you should still specify the intents your plugin needs.
+ * TODO: this feature is unfinished, but you should still specify the intents your plugin needs.  
+ * Never request more permissions than you need, as this is a bad practice and can lead to your plugin being rejected.
  */
 export enum PluginIntent {
   /** Plugin has access to hidden config values */
@@ -139,12 +140,20 @@ export type PluginDef = {
     };
     /** URL to the plugin's icon - recommended size: 48x48 to 128x128 */
     iconUrl?: string;
+    license?: {
+      /** License name */
+      name: string;
+      /** URL to the license text */
+      url: string;
+    };
     /** Homepage URLs for the plugin */
-    homepage?: {
+    homepage: {
       /** Any other homepage URL */
       other?: string;
-      /** URL to the plugin's source code (i.e. Git repo) */
-      source?: string;
+      /** URL to the plugin's source code (i.e. Git repo) - closed source plugins are not officially accepted at the moment. */
+      source: string;
+      /** URL to the plugin's bug tracker page, like GitHub issues */
+      bug?: string;
       /** URL to the plugin's GreasyFork page */
       greasyfork?: string;
       /** URL to the plugin's OpenUserJS page */