Nolovenodie / emby-crx

Emby 增强/美化 插件 (适用于 Chrome 内核浏览器 / EmbyServer)
MIT License
936 stars 82 forks source link

添加了一些可配置项 #17

Open mobclix opened 1 year ago

mobclix commented 1 year ago

添加了一些可配置项:

  1. 可选择展示的媒体库
  2. 可选择展示的最大数量
  3. 可选择是否随机展示

因为写的比较丑陋所以就不pr了,有需要的自取,当然作者要是能帮忙重构那就太感谢了! 顺便推荐一个桌面壁纸软件lively-wallpaper,可以把网页设为桌面,和这个项目可以说是绝配

index.html中在前添加

config.js

class Config{
    constructor() {
        //媒体库id,用逗号分隔。进入媒体库后url里的parentId
        //this.parentId = "5,21463";
                this.parentId = "";
        //滚屏间隔 ms
        this.interval = 8000;
        //是否开启随机
        this.random = true;
        this.itemQuery={
            //返回横幅的最大数量,注意每10个为一轮,一轮不足10个的会被舍弃。例如27则只显示两轮20个
            Limit : 20,//(integer) Optional. The maximum number of records to return

            //一般只需要关注上面几个配置,下面不了解的不需要修改
            /*-------------------------------------------------------------------------------------------------------------------------------*/
            Recursive : true,//(boolean) When searching within folders, this determines whether or not the search will be recursive. true/false
            SortOrder : "Descending",//(string) Sort Order - Ascending,Descending
            Fields : "ProductionYear",//(string) Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines
            EnableUserData : false,//(boolean) Optional, include user data
            ImageTypeLimit : 1,//(integer) Optional, the max number of images to return, per image type
            Fields : "ProductionYear",//(string) Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines
            IncludeItemTypes : "Movie,Series",//(string) Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.
            ImageTypes : "Backdrop",//(string) Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.
            EnableImageTypes : "Logo,Backdrop",//(string) Optional. The image types to include in the output.
            SortBy : "ProductionYear, PremiereDate, SortName",//(string) Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime
            ArtistType : "",//(string) Artist or AlbumArtist
            MaxOfficialRating : "",//(string) Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).
            HasThemeSong : "",//(boolean) Optional filter by items with theme songs.
            HasThemeVideo : "",//(boolean) Optional filter by items with theme videos.
            HasSubtitles : "",//(boolean) Optional filter by items with subtitles.
            HasSpecialFeature : "",//(boolean) Optional filter by items with special features.
            HasTrailer : "",//(boolean) Optional filter by items with trailers.
            AdjacentTo : "",//(string) Optional. Return items that are siblings of a supplied item.
            MinIndexNumber : "",//(integer) Optional filter by minimum index number.
            MaxPlayers : "",//(integer) Optional filter by minimum number of game players.
            ParentIndexNumber : "",//(integer) Optional filter by parent index number.
            HasParentalRating : "",//(integer) Optional filter by items that have or do not have a parental rating
            IsHD : "",//(boolean) Optional filter by items that are HD or not.
            LocationTypes : "",//(string) Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.
            ExcludeLocationTypes : "",//(string) Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.
            IsMissing : "",//(boolean) Optional filter by items that are missing episodes or not.
            IsUnaired : "",//(boolean) Optional filter by items that are unaired episodes or not.
            MinCommunityRating : "",//(number) Optional filter by minimum community rating.
            MinCriticRating : "",//(number) Optional filter by minimum critic rating.
            AiredDuringSeason : "",//(integer) Gets all episodes that aired during a season, including specials.
            MinPremiereDate : "",//(string) Optional. The minimum premiere date. Format = ISO
            MinDateLastSaved : "",//(string) Optional. The minimum premiere date. Format = ISO
            MinDateLastSavedForUser : "",//(string) Optional. The minimum premiere date. Format = ISO
            MaxPremiereDate : "",//(string) Optional. The maximum premiere date. Format = ISO
            HasOverview : "",//(boolean) Optional filter by items that have an overview or not.
            HasImdbId : "",//(boolean) Optional filter by items that have an imdb id or not.
            HasTmdbId : "",//(boolean) Optional filter by items that have a tmdb id or not.
            HasTvdbId : "",//(boolean) Optional filter by items that have a tvdb id or not.
            ExcludeItemIds : "",//(string) Optional. If specified, results will be filtered by exxcluding item ids. This allows multiple, comma delimeted.
            StartIndex : "",//(integer) Optional. The record index to start at. All items with a lower index will be dropped from the results.
            ExcludeItemTypes : "",//(string) Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.
            AnyProviderIdEquals : "",//(string) Optional. If specified, result will be filtered to contain only items which match at least one of the specified IDs. Each provider ID must be in the form 'prov.id', e.g. 'imdb.tt123456'. This allows multiple, comma delimeted value pairs.
            Filters : "",//(string) Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes
            IsFavorite : "",//(boolean) Optional filter by items that are marked as favorite, or not.
            IsMovie : "",//(boolean) Optional filter for movies.
            IsSeries : "",//(boolean) Optional filter for movies.
            IsNews : "",//(boolean) Optional filter for news.
            IsKids : "",//(boolean) Optional filter for kids.
            IsSports : "",//(boolean) Optional filter for sports.
            MediaTypes : "",//(string) Optional filter by MediaType. Allows multiple, comma delimited.
            IsPlayed : "",//(string) Optional filter by items that are played, or not.
            Genres : "",//(string) Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimeted.
            OfficialRatings : "",//(string) Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimeted.
            Tags : "",//(string) Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimeted.
            Years : "",//(string) Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimeted.
            EnableImages : "",//(boolean) Optional, include image information in output
            Person : "",//(string) Optional. If specified, results will be filtered to include only those containing the specified person.
            PersonIds : "",//(string) Optional. If specified, results will be filtered to include only those containing the specified person.
            PersonTypes : "",//(string) Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited
            Studios : "",//(string) Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.
            StudioIds : "",//(string) Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.
            Artists : "",//(string) Optional. If specified, results will be filtered based on artist. This allows multiple, pipe delimeted.
            ArtistIds : "",//(string) Optional. If specified, results will be filtered based on artist. This allows multiple, pipe delimeted.
            Albums : "",//(string) Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimeted.
            Ids : "",//(string) Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.
            VideoTypes : "",//(string) Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimeted.
            Containers : "",//(string) Optional filter by Container. Allows multiple, comma delimeted.
            AudioCodecs : "",//(string) Optional filter by AudioCodec. Allows multiple, comma delimeted.
            VideoCodecs : "",//(string) Optional filter by VideoCodec. Allows multiple, comma delimeted.
            SubtitleCodecs : "",//(string) Optional filter by SubtitleCodec. Allows multiple, comma delimeted.
            Path : "",//(string) Optional filter by Path.
            UserId  : "",//(string) User Id
            MinOfficialRating : "",//(string) Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).
            IsLocked : "",//(string) Optional filter by items that are locked.
            IsPlaceHolder : "",//(boolean) Optional filter by items that are placeholders
            HasOfficialRating : "",//(boolean) Optional filter by items that have official ratings
            GroupItemsIntoCollections : "",//(boolean) Whether or not to hide items behind their boxsets.
            Is3D : "",//(boolean) Whether or not to hide items behind their boxsets.
            SeriesStatus : "",//(string) Optional filter by Series Status. Allows multiple, comma delimeted.
            NameStartsWithOrGreater : "",//(string) Optional filter by items whose name is sorted equally or greater than a given input string.
            NameStartsWith : "",//(string) Optional filter by items whose name is sorted equally than a given input string.
            NameLessThan : ""//(string) Optional filter by items whose name is equally or lesser than a given input string.
        }

        Object.getOwnPropertyNames(this.itemQuery).forEach(key => {
            if(this.itemQuery[key]==""){
                delete this.itemQuery[key];
            }
        })

        this.parentIds = this.parentId.split(',');
    }
}

main.js

class Home {
    static start() {
        this.cache = {
            items: undefined,
            item: new Map(),
        };
        this.config = new Config();
        console.log(this.config.itemQuery);
        this.itemQuery = this.config.itemQuery;
        this.coverOptions = { type: "Backdrop", maxWidth: 3000 };
        this.logoOptions = { type: "Logo", maxWidth: 3000 };

        setInterval(() => {
            if (window.location.href.indexOf("!/home") != -1) {
                if ($(".view:not(.hide) .misty-banner").length == 0 && $(".misty-loading").length == 0) {
                    this.initLoading();
                }
                if ($(".hide .misty-banner").length != 0) {
                    $(".hide .misty-banner").remove();
                }
                if ($(".section0 .card").length != 0 && $(".view:not(.hide) .misty-banner").length == 0) {
                    this.init();
                }
            }
        }, 100);
    }

    static async init() {
        // Beta
        $(".view:not(.hide)").attr("data-type", "home");
        // Loading
        const serverName = await this.injectCall("serverName", "");
        $(".misty-loading h1").text(serverName).addClass("active");
        // Banner
        await this.initBanner();
        this.initEvent();
    }

    /* 插入Loading */
    static initLoading() {
        const load = `
        <div class="misty-loading">
            <h1></h1>
            <div class="mdl-spinner"><div class="mdl-spinner__layer mdl-spinner__layer-1"><div class="mdl-spinner__circle-clipper mdl-spinner__left"><div class="mdl-spinner__circle mdl-spinner__circleLeft"></div></div><div class="mdl-spinner__circle-clipper mdl-spinner__right"><div class="mdl-spinner__circle mdl-spinner__circleRight"></div></div></div></div>
        </div>
        `;
        $("body").append(load);
    }

    static injectCode(code) {
        let hash = md5(code + Math.random().toString());
        return new Promise((resolve, reject) => {
            const channel = new BroadcastChannel(hash);
            channel.addEventListener("message", (event) => resolve(event.data));
            const script = `
            <script class="I${hash}">
                setTimeout(async ()=> {
                    async function R${hash}(){${code}};
                    const channel = new BroadcastChannel("${hash}");
                    channel.postMessage(await R${hash}());
                    document.querySelector("script.I${hash}").remove()
                }, 16)
            </script>
            `;
            $(document.head || document.documentElement).append(script);
        });
    }

    static injectCall(func, arg) {
        const script = `
        // const client = (await window.require(["ApiClient"]))[0];
        const client = await new Promise((resolve, reject) => {
            setInterval(() => {
                if (window.ApiClient != undefined) resolve(window.ApiClient);
            }, 16);
        });
        return await client.${func}(${arg})
        `;
        return this.injectCode(script);
    }

    static getItems(query) {
        //由于要合并多个媒体库所以放弃做缓存
        return this.injectCall("getItems", "client.getCurrentUserId(), " + JSON.stringify(query));

    }

    static itemsRandom(array){
        let res = [], random;
        while(array.length>0){
            random = Math.floor(Math.random()*array.length);
            res.push(array[random]);
            array.splice(random, 1);
        }
        return res;
    }

    static async getItem(itemId) {
        // 双缓存 优先使用 WebStorage
        if (typeof Storage !== "undefined" && !localStorage.getItem("CACHE|" + itemId) && !this.cache.item.has(itemId)) {
            const data = JSON.stringify(await this.injectCall("getItem", `client.getCurrentUserId(), "${itemId}"`));
            if (typeof Storage !== "undefined") localStorage.setItem("CACHE|" + itemId, data);
            else this.cache.item.set(itemId, data);
        }
        return JSON.parse(typeof Storage !== "undefined" ? localStorage.getItem("CACHE|" + itemId) : this.cache.item.get(itemId));
    }

    static getImageUrl(itemId, options) {
        return this.injectCall("getImageUrl", itemId + ", " + JSON.stringify(options));
    }

    static async appendItem(i){
        const detail = await this.getItem(this.data.Items[i].Id),
                itemHtml = `
            <div class="misty-banner-item" id="${detail.Id}">
                <img draggable="false" loading="eager" decoding="async" class="misty-banner-cover" src="${await this.getImageUrl(detail.Id, this.coverOptions)}" alt="Backdrop" style="">
                <div class="misty-banner-info padded-left padded-right">
                    <h1>${detail.Name}</h1>
                    <div><p>${detail.Overview}</p></div>
                    <div><button onclick="appRouter.showItem('${detail.Id}')">MORE</button></div>
                </div>
            </div>
            `,
                logoHtml = `
            <img id="${detail.Id}" draggable="false" loading="auto" decoding="lazy" class="misty-banner-logo" data-banner="img-title" alt="Logo" src="${await this.getImageUrl(detail.Id, this.logoOptions)}">
            `;
            if (detail.ImageTags && detail.ImageTags.Logo) {
                $(".misty-banner-logos").append(logoHtml);
            }
            $(".misty-banner-body").append(itemHtml);
    }

    //总数小于10
    static bannerRollOnce(){
        this.index += this.index + 1 == $(".misty-banner-item").length ? -this.index : 1;
        $(".misty-banner-body").css("left", -(this.index * 100).toString() + "%");
        // 信息切换
        $(".misty-banner-item.active").removeClass("active");
        let id = $(".misty-banner-item").eq(this.index).addClass("active").attr("id");
        // LOGO切换
        $(".misty-banner-logo.active").removeClass("active");
        $(`.misty-banner-logo[id=${id}]`).addClass("active");
    }

    //总数大于10
    static async bannerRoll(){
        // 背景切换
        this.index += this.index + 1 == $(".misty-banner-item").length ? -this.index : 1;
        //正常切换
        if(this.index != 0){
            $(".misty-banner-body").css("left", -(this.index * 100).toString() + "%");
            // 信息切换
            $(".misty-banner-item.active").removeClass("active");
            let id = $(".misty-banner-item").eq(this.index).addClass("active").attr("id");
            // LOGO切换
            $(".misty-banner-logo.active").removeClass("active");
            $(`.misty-banner-logo[id=${id}]`).addClass("active");
        }
        //已经切换到最后
        if(this.index == 0){
            clearInterval(this.bannerInterval);
            let l = $(".misty-banner-item").length;
            //剩余小于10张直接舍弃,从头开始
            if(this.count+1==this.data.Items.length||this.data.Items.length-this.count-1<10)
                this.count=-1;
            //向后添加,剩余10张以上添加10张
            for(let i=this.count+1;i<this.count+1+(this.data.Items.length-this.count-1<10?this.data.Items.length-this.count-1:10);i++){
                await this.appendItem(i)
            }
            //切换到第一张
            $(".misty-banner-body").css("left", "0%");
            $(".misty-banner-item.active").removeClass("active");
            let id = $(".misty-banner-item").eq(l).addClass("active").attr("id");
            $(".misty-banner-logo.active").removeClass("active");
            $(`.misty-banner-logo[id=${id}]`).addClass("active");
            //从dom中移除上一轮次的横幅
            for(let i=0;i<l;i++){
                $(".misty-banner-item").eq(0).remove();
            }
            this.bannerInterval = setInterval(this.bannerRoll.bind(this), this.config.interval);
        }
        this.count++;
    }
    /* 插入Banner */
    static async initBanner() {
        const banner = `
        <div class="misty-banner">
            <div class="misty-banner-body">
            </div>
            <div class="misty-banner-library">
                <div class="misty-banner-logos"></div>
            </div>
        </div>
        `;
        $(".view:not(.hide) .homeSectionsContainer").prepend(banner);
        $(".view:not(.hide) .section0").detach().appendTo(".view:not(.hide) .misty-banner-library");

        // 插入数据
        this.data = {Items:[]}
        //配置的媒体库不为空
        if(this.config.parentIds[0] != ""){
            //合并所有配置的媒体库的结果
            for(let parentId of this.config.parentIds){
                this.itemQuery.ParentId = parentId;
                let res = await this.getItems(this.itemQuery);
                this.data.Items = this.data.Items.concat(res.Items);
            }
        } else {
            //查询所有媒体库
            this.data = await this.getItems(this.itemQuery);
        }

        if(this.config.random==true){
            this.data.Items = this.itemsRandom(this.data.Items);
        }
        //大于10时添加10张                   
        for(let i=0;i<(this.data.Items.length<10?this.data.Items.length:10);i++){
            await this.appendItem(i)
            //console.log(item.Id, detail);
        }
        let complete = 0;
        let loading = setInterval(async () => {
            // 判断图片加载完毕
            $(".misty-banner-cover:not(.complete)").each((i, dom) => {
                if (dom.complete) {
                    dom.classList.add("complete");
                    complete++;
                }
            });
            if (complete == $(".misty-banner-item").length && $(".misty-banner-item").length != 0) {
                clearInterval(loading);
                $(".misty-loading").fadeOut(500, () => $(".misty-loading").remove());
                await CommonUtils.sleep(150);
                // 置入场动画
                let delay = 80; // 动媒体库画间隔
                let id = $(".misty-banner-item").eq(0).addClass("active").attr("id"); // 初次信息动画
                $(`.misty-banner-logo[id=${id}]`).addClass("active");

                await CommonUtils.sleep(200); // 间隔动画
                $(".section0 > div").addClass("misty-banner-library-overflow"); // 关闭overflow 防止媒体库动画溢出
                $(".misty-banner .card").each((i, dom) => setTimeout(() => $(dom).addClass("misty-banner-library-show"), i * delay)); // 媒体库动画
                await CommonUtils.sleep(delay * 8 + 1000); // 等待媒体库动画完毕
                $(".section0 > div").removeClass("misty-banner-library-overflow"); // 开启overflow 防止无法滚动

                // 滚屏逻辑
                clearInterval(this.bannerInterval);
                this.index = 0;this.count = 0;
                this.bannerInterval = this.data.Items.length<10?setInterval(this.bannerRollOnce.bind(this), this.config.interval):setInterval(this.bannerRoll.bind(this), this.config.interval);
            }
        }, 16);
    }

    /* 初始事件 */
    static initEvent() {
        // 通过注入方式, 方可调用appRouter函数, 以解决Content-Script window对象不同步问题
        const script = `
        // 挂载appRouter
        if (!window.appRouter) window.appRouter = (await window.require(["appRouter"]))[0];
        // 修复library事件参数
        const serverId = ApiClient._serverInfo.Id,
            librarys = document.querySelectorAll(".view:not(.hide) .section0 .card");
        librarys.forEach(library => {
            library.setAttribute("data-serverid", serverId);
            library.setAttribute("data-type", "CollectionFolder");
        });
        `;
        this.injectCode(script);
    }
}

// 运行
if ($("meta[name=application-name]").attr("content") == "Emby" || $(".accent-emby") != undefined) {
    Home.start();
}
AjianNie commented 1 year ago

秀,感谢分享!

Nolovenodie commented 1 year ago

牛的, 没做配置项, 主要是配置项有点多, 如果做成界面可调节的, 太麻烦了, 我是懒狗哈哈哈....

yue631377105 commented 1 year ago

大佬,您好,用这个后,是可以选择展示的媒体库了,但是不知道为什么,主页会生成两个展示大图,就是鼠标向下拉后,会有额外的一个一模一样的展示图,下方也带有一模一样的媒体库,都能点击进去,而且主页大图在轮换的时候会重复轮回一次,然后才切换到下一张,这个需要修改什么参数吗? 配置只改了this.parentId = "5";,其他未动。也未设置其他的CSS。

AjianNie commented 1 year ago

大佬,您好,用这个后,是可以选择展示的媒体库了,但是不知道为什么,主页会生成两个展示大图,就是鼠标向下拉后,会有额外的一个一模一样的展示图,下方也带有一模一样的媒体库,都能点击进去,而且主页大图在轮换的时候会重复轮回一次,然后才切换到下一张,这个需要修改什么参数吗? 配置只改了this.parentId = "5";,其他未动。也未设置其他的CSS。

该issue提供的配置与项目本身应该是不同步的,你的这个问题在后者中已修复,参考后者进行修改

yue631377105 commented 1 year ago

大佬,您好,用这个后,是可以选择展示的媒体库了,但是不知道为什么,主页会生成两个展示大图,就是鼠标向下拉后,会有额外的一个一模一样的展示图,下方也带有一模一样的媒体库,都能点击进去,而且主页大图在轮换的时候会重复轮回一次,然后才切换到下一张,这个需要修改什么参数吗? 配置只改了this.parentId = "5";,其他未动。也未设置其他的CSS。

该issue提供的配置与项目本身应该是不同步的,你的这个问题在后者中已修复,参考后者进行修改

感谢回复,使用最新版的是解决了两个主页的问题,不过新的问题是,又不能自由的选择媒体库了,是不是大佬的config.js和作者大佬的main.js是不兼容的。

yue631377105 commented 1 year ago

大佬,您好,用这个后,是可以选择展示的媒体库了,但是不知道为什么,主页会生成两个展示大图,就是鼠标向下拉后,会有额外的一个一模一样的展示图,下方也带有一模一样的媒体库,都能点击进去,而且主页大图在轮换的时候会重复轮回一次,然后才切换到下一张,这个需要修改什么参数吗? 配置只改了this.parentId = "5";,其他未动。也未设置其他的CSS。

该issue提供的配置与项目本身应该是不同步的,你的这个问题在后者中已修复,参考后者进行修改

感谢,修改了前段,可以了。

class Home { static start() { this.cache = { items: undefined, item: new Map(), }; this.config = new Config(); console.log(this.config.itemQuery); this.itemQuery = this.config.itemQuery; this.coverOptions = { type: "Backdrop", maxWidth: 3000 }; this.logoOptions = { type: "Logo", maxWidth: 3000 }; this.initStart = false;

    setInterval(() => {
        if (window.location.href.indexOf("!/home") != -1) {
            if ($(".view:not(.hide) .misty-banner").length == 0 && $(".misty-loading").length == 0) {
                this.initStart = false;
                this.initLoading();
            }
            if ($(".hide .misty-banner").length != 0) {
                $(".hide .misty-banner").remove();
            }
            if (!this.initStart && $(".section0 .card").length != 0 && $(".view:not(.hide) .misty-banner").length == 0) {
                this.initStart = true;
                this.init();
            }
        }
mobclix commented 1 year ago

大佬,您好,用这个后,是可以选择展示的媒体库了,但是不知道为什么,主页会生成两个展示大图,就是鼠标向下拉后,会有额外的一个一模一样的展示图,下方也带有一模一样的媒体库,都能点击进去,而且主页大图在轮换的时候会重复轮回一次,然后才切换到下一张,这个需要修改什么参数吗? 配置只改了this.parentId = "5";,其他未动。也未设置其他的CSS。

该issue提供的配置与项目本身应该是不同步的,你的这个问题在后者中已修复,参考后者进行修改

感谢回复,使用最新版的是解决了两个主页的问题,不过新的问题是,又不能自由的选择媒体库了,是不是大佬的config.js和作者大佬的main.js是不兼容的。

已合并项目最新更新

main.js

class Home {
    static start() {
        this.cache = {
            items: undefined,
            item: new Map(),
        };
        this.config = new Config();
        this.itemQuery = this.config.itemQuery;
        this.coverOptions = { type: "Backdrop", maxWidth: 3000 };
        this.logoOptions = { type: "Logo", maxWidth: 3000 };
        this.initStart = false;
        setInterval(() => {
            if (window.location.href.indexOf("!/home") != -1) {
                if ($(".view:not(.hide) .misty-banner").length == 0 && $(".misty-loading").length == 0) {
                    this.initStart = false;
                    this.initLoading();
                }
                if ($(".hide .misty-banner").length != 0) {
                    $(".hide .misty-banner").remove();
                }
                if (!this.initStart && $(".section0 .card").length != 0 && $(".view:not(.hide) .misty-banner").length == 0) {
                    this.initStart = true;
                    this.init();
                }
            }
        }, 100);
    }

    static async init() {
        // Beta
        $(".view:not(.hide)").attr("data-type", "home");
        // Loading
        const serverName = await this.injectCall("serverName", "");
        $(".misty-loading h1").text(serverName).addClass("active");
        // Banner
        await this.initBanner();
        this.initEvent();
    }

    /* 插入Loading */
    static initLoading() {
        const load = `
        <div class="misty-loading">
            <h1></h1>
            <div class="mdl-spinner"><div class="mdl-spinner__layer mdl-spinner__layer-1"><div class="mdl-spinner__circle-clipper mdl-spinner__left"><div class="mdl-spinner__circle mdl-spinner__circleLeft"></div></div><div class="mdl-spinner__circle-clipper mdl-spinner__right"><div class="mdl-spinner__circle mdl-spinner__circleRight"></div></div></div></div>
        </div>
        `;
        $("body").append(load);
    }

    static injectCode(code) {
        let hash = md5(code + Math.random().toString());
        return new Promise((resolve, reject) => {
            if ("BroadcastChannel" in window) {
                const channel = new BroadcastChannel(hash);
                channel.addEventListener("message", (event) => resolve(event.data));
            } else if ("postMessage" in window) {
                window.addEventListener("message", (event) => {
                    if (event.data.channel === hash) {
                        resolve(event.data.message);
                    }
                });
            }
            const script = `
            <script class="I${hash}">
                setTimeout(async ()=> {
                    async function R${hash}(){${code}};
                    if ("BroadcastChannel" in window) {
                        const channel = new BroadcastChannel("${hash}");
                        channel.postMessage(await R${hash}());
                    } else if ('postMessage' in window) {
                        window.parent.postMessage({channel:"${hash}",message:await R${hash}()}, "*");
                    }
                    document.querySelector("script.I${hash}").remove()
                }, 16)
            </script>
            `;
            $(document.head || document.documentElement).append(script);
        });
    }

    static injectCall(func, arg) {
        const script = `
        // const client = (await window.require(["ApiClient"]))[0];
        const client = await new Promise((resolve, reject) => {
            setInterval(() => {
                if (window.ApiClient != undefined) resolve(window.ApiClient);
            }, 16);
        });
        return await client.${func}(${arg})
        `;
        return this.injectCode(script);
    }

    static getItems(query) {
        //由于要合并多个媒体库所以放弃做缓存
        return this.injectCall("getItems", "client.getCurrentUserId(), " + JSON.stringify(query));
    }

    static itemsRandom(array){
        let res = [], random;
        while(array.length>0){
            random = Math.floor(Math.random()*array.length);
            res.push(array[random]);
            array.splice(random, 1);
        }
        return res;
    }

    static async getItem(itemId) {
        // 双缓存 优先使用 WebStorage
        if (typeof Storage !== "undefined" && !localStorage.getItem("CACHE|" + itemId) && !this.cache.item.has(itemId)) {
            const data = JSON.stringify(await this.injectCall("getItem", `client.getCurrentUserId(), "${itemId}"`));
            if (typeof Storage !== "undefined") localStorage.setItem("CACHE|" + itemId, data);
            else this.cache.item.set(itemId, data);
        }
        return JSON.parse(typeof Storage !== "undefined" ? localStorage.getItem("CACHE|" + itemId) : this.cache.item.get(itemId));
    }

    static getImageUrl(itemId, options) {
        return this.injectCall("getImageUrl", itemId + ", " + JSON.stringify(options));
    }

    static async appendItem(i){
        const detail = await this.getItem(this.data.Items[i].Id),
                itemHtml = `
            <div class="misty-banner-item" id="${detail.Id}">
                <img draggable="false" loading="eager" decoding="async" class="misty-banner-cover" src="${await this.getImageUrl(detail.Id, this.coverOptions)}" alt="Backdrop" style="">
                <div class="misty-banner-info padded-left padded-right">
                    <h1>${detail.Name}</h1>
                    <div><p>${detail.Overview}</p></div>
                    <div><button onclick="appRouter.showItem('${detail.Id}')">MORE</button></div>
                </div>
            </div>
            `,
                logoHtml = `
            <img id="${detail.Id}" draggable="false" loading="auto" decoding="lazy" class="misty-banner-logo" data-banner="img-title" alt="Logo" src="${await this.getImageUrl(detail.Id, this.logoOptions)}">
            `;
            if (detail.ImageTags && detail.ImageTags.Logo) {
                $(".misty-banner-logos").append(logoHtml);
            }
            $(".misty-banner-body").append(itemHtml);
    }

    static async bannerRoll(){
        // 背景切换
        this.index += this.index + 1 == $(".misty-banner-item").length ? -this.index : 1;
        //已经切换到最后且总数大于10
        if(this.index == 0 && this.data.Items.length >= 10){
            clearInterval(this.bannerInterval);
            let l = $(".misty-banner-item").length;
            //剩余小于10张直接舍弃,从头开始
            if(this.count+1==this.data.Items.length||this.data.Items.length-this.count-1<10)
                this.count=-1;
            //向后添加,剩余10张以上添加10张
            for(let i=this.count+1;i<this.count+1+(this.data.Items.length-this.count-1<10?this.data.Items.length-this.count-1:10);i++){
                await this.appendItem(i)
            }
            //切换到第一张
            $(".misty-banner-body").css("left", "0%");
            $(".misty-banner-item.active").removeClass("active");
            let id = $(".misty-banner-item").eq(l).addClass("active").attr("id");
            $(".misty-banner-logo.active").removeClass("active");
            $(`.misty-banner-logo[id=${id}]`).addClass("active");
            //从dom中移除上一轮次的横幅
            for(let i=0;i<l;i++){
                $(".misty-banner-item").eq(0).remove();
            }
            this.bannerInterval = setInterval(this.bannerRoll.bind(this), this.config.interval);
        } else {
            $(".misty-banner-body").css("left", -(this.index * 100).toString() + "%");
            // 信息切换
            $(".misty-banner-item.active").removeClass("active");
            let id = $(".misty-banner-item").eq(this.index).addClass("active").attr("id");
            // LOGO切换
            $(".misty-banner-logo.active").removeClass("active");
            $(`.misty-banner-logo[id=${id}]`).addClass("active");
        }
        this.count++;
    }

    /* 插入Banner */
    static async initBanner() {
        const banner = `
        <div class="misty-banner">
            <div class="misty-banner-body">
            </div>
            <div class="misty-banner-library">
                <div class="misty-banner-logos"></div>
            </div>
        </div>
        `;
        $(".view:not(.hide) .homeSectionsContainer").prepend(banner);
        $(".view:not(.hide) .section0").detach().appendTo(".view:not(.hide) .misty-banner-library");

        // 插入数据
        this.data = {Items:[]}
        //配置的媒体库不为空
        if(this.config.parentIds[0] != ""){
            //合并所有配置的媒体库的结果
            for(let parentId of this.config.parentIds){
                this.itemQuery.ParentId = parentId;
                let res = await this.getItems(this.itemQuery);
                this.data.Items = this.data.Items.concat(res.Items);
            }
        } else {
            //查询所有媒体库
            this.data = await this.getItems(this.itemQuery);
        }

        if(this.config.random==true){
            this.data.Items = this.itemsRandom(this.data.Items);
        }
        //大于10时添加10张                   
        for(let i=0;i<(this.data.Items.length<10?this.data.Items.length:10);i++){
            await this.appendItem(i)
        }

        // 只判断第一张海报加载完毕, 优化加载速度
        await new Promise((resolve, reject) => {
            let waitLoading = setInterval(() => {
                if (document.querySelector(".misty-banner-cover").complete) {
                    clearInterval(waitLoading);
                    resolve();
                }
            }, 16);
        });

        $(".misty-loading").fadeOut(500, () => $(".misty-loading").remove());
        await CommonUtils.sleep(150);
        // 置入场动画
        let delay = 80; // 动媒体库画间隔
        let id = $(".misty-banner-item").eq(0).addClass("active").attr("id"); // 初次信息动画
        $(`.misty-banner-logo[id=${id}]`).addClass("active");

        await CommonUtils.sleep(200); // 间隔动画
        $(".section0 > div").addClass("misty-banner-library-overflow"); // 关闭overflow 防止媒体库动画溢出
        $(".misty-banner .card").each((i, dom) => setTimeout(() => $(dom).addClass("misty-banner-library-show"), i * delay)); // 媒体库动画
        await CommonUtils.sleep(delay * 8 + 1000); // 等待媒体库动画完毕
        $(".section0 > div").removeClass("misty-banner-library-overflow"); // 开启overflow 防止无法滚动

        // 滚屏逻辑
        this.index = 0;this.count = 0;
        clearInterval(this.bannerInterval);
        this.bannerInterval = setInterval(this.bannerRoll.bind(this), this.config.interval);
    }

    /* 初始事件 */
    static initEvent() {
        // 通过注入方式, 方可调用appRouter函数, 以解决Content-Script window对象不同步问题
        const script = `
        // 挂载appRouter
        if (!window.appRouter) window.appRouter = (await window.require(["appRouter"]))[0];
        // 修复library事件参数
        const serverId = ApiClient._serverInfo.Id,
            librarys = document.querySelectorAll(".view:not(.hide) .section0 .card");
        librarys.forEach(library => {
            library.setAttribute("data-serverid", serverId);
            library.setAttribute("data-type", "CollectionFolder");
        });
        `;
        this.injectCode(script);
    }
}

// 运行
if ("BroadcastChannel" in window || "postMessage" in window) {
    if ($("meta[name=application-name]").attr("content") == "Emby" || $(".accent-emby") != undefined) {
        Home.start();
    }
}
jackloves111 commented 1 year ago

感谢大佬,更新之后好多了。版本好像没有建标签,直接用的master

yue631377105 commented 1 year ago

大佬,您好,用这个后,是可以选择展示的媒体库了,但是不知道为什么,主页会生成两个展示大图,就是鼠标向下拉后,会有额外的一个一模一样的展示图,下方也带有一模一样的媒体库,都能点击进去,而且主页大图在轮换的时候会重复轮回一次,然后才切换到下一张,这个需要修改什么参数吗? 配置只改了this.parentId = "5";,其他未动。也未设置其他的CSS。

该issue提供的配置与项目本身应该是不同步的,你的这个问题在后者中已修复,参考后者进行修改

感谢回复,使用最新版的是解决了两个主页的问题,不过新的问题是,又不能自由的选择媒体库了,是不是大佬的config.js和作者大佬的main.js是不兼容的。

已合并项目最新更新

main.js

class Home {
  static start() {
      this.cache = {
          items: undefined,
          item: new Map(),
      };
      this.config = new Config();
      this.itemQuery = this.config.itemQuery;
      this.coverOptions = { type: "Backdrop", maxWidth: 3000 };
      this.logoOptions = { type: "Logo", maxWidth: 3000 };
      this.initStart = false;
      setInterval(() => {
          if (window.location.href.indexOf("!/home") != -1) {
              if ($(".view:not(.hide) .misty-banner").length == 0 && $(".misty-loading").length == 0) {
                  this.initStart = false;
                  this.initLoading();
              }
              if ($(".hide .misty-banner").length != 0) {
                  $(".hide .misty-banner").remove();
              }
              if (!this.initStart && $(".section0 .card").length != 0 && $(".view:not(.hide) .misty-banner").length == 0) {
                  this.initStart = true;
                  this.init();
              }
          }
      }, 100);
  }

  static async init() {
      // Beta
      $(".view:not(.hide)").attr("data-type", "home");
      // Loading
      const serverName = await this.injectCall("serverName", "");
      $(".misty-loading h1").text(serverName).addClass("active");
      // Banner
      await this.initBanner();
      this.initEvent();
  }

  /* 插入Loading */
  static initLoading() {
      const load = `
      <div class="misty-loading">
          <h1></h1>
          <div class="mdl-spinner"><div class="mdl-spinner__layer mdl-spinner__layer-1"><div class="mdl-spinner__circle-clipper mdl-spinner__left"><div class="mdl-spinner__circle mdl-spinner__circleLeft"></div></div><div class="mdl-spinner__circle-clipper mdl-spinner__right"><div class="mdl-spinner__circle mdl-spinner__circleRight"></div></div></div></div>
      </div>
      `;
      $("body").append(load);
  }

  static injectCode(code) {
      let hash = md5(code + Math.random().toString());
      return new Promise((resolve, reject) => {
          if ("BroadcastChannel" in window) {
              const channel = new BroadcastChannel(hash);
              channel.addEventListener("message", (event) => resolve(event.data));
          } else if ("postMessage" in window) {
              window.addEventListener("message", (event) => {
                  if (event.data.channel === hash) {
                      resolve(event.data.message);
                  }
              });
          }
          const script = `
          <script class="I${hash}">
              setTimeout(async ()=> {
                  async function R${hash}(){${code}};
                  if ("BroadcastChannel" in window) {
                      const channel = new BroadcastChannel("${hash}");
                      channel.postMessage(await R${hash}());
                  } else if ('postMessage' in window) {
                      window.parent.postMessage({channel:"${hash}",message:await R${hash}()}, "*");
                  }
                  document.querySelector("script.I${hash}").remove()
              }, 16)
          </script>
          `;
          $(document.head || document.documentElement).append(script);
      });
  }

  static injectCall(func, arg) {
      const script = `
      // const client = (await window.require(["ApiClient"]))[0];
      const client = await new Promise((resolve, reject) => {
          setInterval(() => {
              if (window.ApiClient != undefined) resolve(window.ApiClient);
          }, 16);
      });
      return await client.${func}(${arg})
      `;
      return this.injectCode(script);
  }

  static getItems(query) {
      //由于要合并多个媒体库所以放弃做缓存
      return this.injectCall("getItems", "client.getCurrentUserId(), " + JSON.stringify(query));
  }

  static itemsRandom(array){
      let res = [], random;
      while(array.length>0){
          random = Math.floor(Math.random()*array.length);
          res.push(array[random]);
          array.splice(random, 1);
      }
      return res;
  }

  static async getItem(itemId) {
      // 双缓存 优先使用 WebStorage
      if (typeof Storage !== "undefined" && !localStorage.getItem("CACHE|" + itemId) && !this.cache.item.has(itemId)) {
          const data = JSON.stringify(await this.injectCall("getItem", `client.getCurrentUserId(), "${itemId}"`));
          if (typeof Storage !== "undefined") localStorage.setItem("CACHE|" + itemId, data);
          else this.cache.item.set(itemId, data);
      }
      return JSON.parse(typeof Storage !== "undefined" ? localStorage.getItem("CACHE|" + itemId) : this.cache.item.get(itemId));
  }

  static getImageUrl(itemId, options) {
      return this.injectCall("getImageUrl", itemId + ", " + JSON.stringify(options));
  }

  static async appendItem(i){
      const detail = await this.getItem(this.data.Items[i].Id),
              itemHtml = `
          <div class="misty-banner-item" id="${detail.Id}">
              <img draggable="false" loading="eager" decoding="async" class="misty-banner-cover" src="${await this.getImageUrl(detail.Id, this.coverOptions)}" alt="Backdrop" style="">
              <div class="misty-banner-info padded-left padded-right">
                  <h1>${detail.Name}</h1>
                  <div><p>${detail.Overview}</p></div>
                  <div><button onclick="appRouter.showItem('${detail.Id}')">MORE</button></div>
              </div>
          </div>
          `,
              logoHtml = `
          <img id="${detail.Id}" draggable="false" loading="auto" decoding="lazy" class="misty-banner-logo" data-banner="img-title" alt="Logo" src="${await this.getImageUrl(detail.Id, this.logoOptions)}">
          `;
          if (detail.ImageTags && detail.ImageTags.Logo) {
              $(".misty-banner-logos").append(logoHtml);
          }
          $(".misty-banner-body").append(itemHtml);
  }

  static async bannerRoll(){
      // 背景切换
      this.index += this.index + 1 == $(".misty-banner-item").length ? -this.index : 1;
      //已经切换到最后且总数大于10
      if(this.index == 0 && this.data.Items.length >= 10){
          clearInterval(this.bannerInterval);
          let l = $(".misty-banner-item").length;
          //剩余小于10张直接舍弃,从头开始
          if(this.count+1==this.data.Items.length||this.data.Items.length-this.count-1<10)
              this.count=-1;
          //向后添加,剩余10张以上添加10张
          for(let i=this.count+1;i<this.count+1+(this.data.Items.length-this.count-1<10?this.data.Items.length-this.count-1:10);i++){
              await this.appendItem(i)
          }
          //切换到第一张
          $(".misty-banner-body").css("left", "0%");
          $(".misty-banner-item.active").removeClass("active");
          let id = $(".misty-banner-item").eq(l).addClass("active").attr("id");
          $(".misty-banner-logo.active").removeClass("active");
          $(`.misty-banner-logo[id=${id}]`).addClass("active");
          //从dom中移除上一轮次的横幅
          for(let i=0;i<l;i++){
              $(".misty-banner-item").eq(0).remove();
          }
          this.bannerInterval = setInterval(this.bannerRoll.bind(this), this.config.interval);
      } else {
          $(".misty-banner-body").css("left", -(this.index * 100).toString() + "%");
          // 信息切换
          $(".misty-banner-item.active").removeClass("active");
          let id = $(".misty-banner-item").eq(this.index).addClass("active").attr("id");
          // LOGO切换
          $(".misty-banner-logo.active").removeClass("active");
          $(`.misty-banner-logo[id=${id}]`).addClass("active");
      }
      this.count++;
  }

  /* 插入Banner */
  static async initBanner() {
      const banner = `
      <div class="misty-banner">
          <div class="misty-banner-body">
          </div>
          <div class="misty-banner-library">
              <div class="misty-banner-logos"></div>
          </div>
      </div>
      `;
      $(".view:not(.hide) .homeSectionsContainer").prepend(banner);
      $(".view:not(.hide) .section0").detach().appendTo(".view:not(.hide) .misty-banner-library");

      // 插入数据
      this.data = {Items:[]}
      //配置的媒体库不为空
      if(this.config.parentIds[0] != ""){
          //合并所有配置的媒体库的结果
          for(let parentId of this.config.parentIds){
              this.itemQuery.ParentId = parentId;
              let res = await this.getItems(this.itemQuery);
              this.data.Items = this.data.Items.concat(res.Items);
          }
      } else {
          //查询所有媒体库
          this.data = await this.getItems(this.itemQuery);
      }

      if(this.config.random==true){
          this.data.Items = this.itemsRandom(this.data.Items);
      }
      //大于10时添加10张                   
      for(let i=0;i<(this.data.Items.length<10?this.data.Items.length:10);i++){
          await this.appendItem(i)
      }

      // 只判断第一张海报加载完毕, 优化加载速度
      await new Promise((resolve, reject) => {
          let waitLoading = setInterval(() => {
              if (document.querySelector(".misty-banner-cover").complete) {
                  clearInterval(waitLoading);
                  resolve();
              }
          }, 16);
      });

      $(".misty-loading").fadeOut(500, () => $(".misty-loading").remove());
      await CommonUtils.sleep(150);
      // 置入场动画
      let delay = 80; // 动媒体库画间隔
      let id = $(".misty-banner-item").eq(0).addClass("active").attr("id"); // 初次信息动画
      $(`.misty-banner-logo[id=${id}]`).addClass("active");

      await CommonUtils.sleep(200); // 间隔动画
      $(".section0 > div").addClass("misty-banner-library-overflow"); // 关闭overflow 防止媒体库动画溢出
      $(".misty-banner .card").each((i, dom) => setTimeout(() => $(dom).addClass("misty-banner-library-show"), i * delay)); // 媒体库动画
      await CommonUtils.sleep(delay * 8 + 1000); // 等待媒体库动画完毕
      $(".section0 > div").removeClass("misty-banner-library-overflow"); // 开启overflow 防止无法滚动

      // 滚屏逻辑
      this.index = 0;this.count = 0;
      clearInterval(this.bannerInterval);
      this.bannerInterval = setInterval(this.bannerRoll.bind(this), this.config.interval);
  }

  /* 初始事件 */
  static initEvent() {
      // 通过注入方式, 方可调用appRouter函数, 以解决Content-Script window对象不同步问题
      const script = `
      // 挂载appRouter
      if (!window.appRouter) window.appRouter = (await window.require(["appRouter"]))[0];
      // 修复library事件参数
      const serverId = ApiClient._serverInfo.Id,
          librarys = document.querySelectorAll(".view:not(.hide) .section0 .card");
      librarys.forEach(library => {
          library.setAttribute("data-serverid", serverId);
          library.setAttribute("data-type", "CollectionFolder");
      });
      `;
      this.injectCode(script);
  }
}

// 运行
if ("BroadcastChannel" in window || "postMessage" in window) {
  if ($("meta[name=application-name]").attr("content") == "Emby" || $(".accent-emby") != undefined) {
      Home.start();
  }
}

非常感谢大佬,现在要是再适配手机版本的,真的就非常完美了, 在电报群里找到了一个适配手机的,但是MORE被修改成了PLAY,原版是进入详情,修改成了直接播放,对于使用外置播放器的,不是很友好。 那个也可以指定媒体库展示,但是不能选择多个媒体库,好像图片也是不是随机刷新的。 要是合并起来,大佬的可配置性强,电报大佬的手机适配好,绝了。 非常感谢原作者提供这个好的项目。

AjianNie commented 1 year ago

大佬,您好,用这个后,是可以选择展示的媒体库了,但是不知道为什么,主页会生成两个展示大图,就是鼠标向下拉后,会有额外的一个一模一样的展示图,下方也带有一模一样的媒体库,都能点击进去,而且主页大图在轮换的时候会重复轮回一次,然后才切换到下一张,这个需要修改什么参数吗? 配置只改了this.parentId = "5";,其他未动。也未设置其他的CSS。

该issue提供的配置与项目本身应该是不同步的,你的这个问题在后者中已修复,参考后者进行修改

感谢回复,使用最新版的是解决了两个主页的问题,不过新的问题是,又不能自由的选择媒体库了,是不是大佬的config.js和作者大佬的main.js是不兼容的。

已合并项目最新更新

main.js

class Home {
    static start() {
        this.cache = {
            items: undefined,
            item: new Map(),
        };
        this.config = new Config();
        this.itemQuery = this.config.itemQuery;
        this.coverOptions = { type: "Backdrop", maxWidth: 3000 };
        this.logoOptions = { type: "Logo", maxWidth: 3000 };
        this.initStart = false;
        setInterval(() => {
            if (window.location.href.indexOf("!/home") != -1) {
                if ($(".view:not(.hide) .misty-banner").length == 0 && $(".misty-loading").length == 0) {
                    this.initStart = false;
                    this.initLoading();
                }
                if ($(".hide .misty-banner").length != 0) {
                    $(".hide .misty-banner").remove();
                }
                if (!this.initStart && $(".section0 .card").length != 0 && $(".view:not(.hide) .misty-banner").length == 0) {
                    this.initStart = true;
                    this.init();
                }
            }
        }, 100);
    }

    static async init() {
        // Beta
        $(".view:not(.hide)").attr("data-type", "home");
        // Loading
        const serverName = await this.injectCall("serverName", "");
        $(".misty-loading h1").text(serverName).addClass("active");
        // Banner
        await this.initBanner();
        this.initEvent();
    }

    /* 插入Loading */
    static initLoading() {
        const load = `
        <div class="misty-loading">
            <h1></h1>
            <div class="mdl-spinner"><div class="mdl-spinner__layer mdl-spinner__layer-1"><div class="mdl-spinner__circle-clipper mdl-spinner__left"><div class="mdl-spinner__circle mdl-spinner__circleLeft"></div></div><div class="mdl-spinner__circle-clipper mdl-spinner__right"><div class="mdl-spinner__circle mdl-spinner__circleRight"></div></div></div></div>
        </div>
        `;
        $("body").append(load);
    }

    static injectCode(code) {
        let hash = md5(code + Math.random().toString());
        return new Promise((resolve, reject) => {
            if ("BroadcastChannel" in window) {
                const channel = new BroadcastChannel(hash);
                channel.addEventListener("message", (event) => resolve(event.data));
            } else if ("postMessage" in window) {
                window.addEventListener("message", (event) => {
                    if (event.data.channel === hash) {
                        resolve(event.data.message);
                    }
                });
            }
            const script = `
            <script class="I${hash}">
                setTimeout(async ()=> {
                    async function R${hash}(){${code}};
                    if ("BroadcastChannel" in window) {
                        const channel = new BroadcastChannel("${hash}");
                        channel.postMessage(await R${hash}());
                    } else if ('postMessage' in window) {
                        window.parent.postMessage({channel:"${hash}",message:await R${hash}()}, "*");
                    }
                    document.querySelector("script.I${hash}").remove()
                }, 16)
            </script>
            `;
            $(document.head || document.documentElement).append(script);
        });
    }

    static injectCall(func, arg) {
        const script = `
        // const client = (await window.require(["ApiClient"]))[0];
        const client = await new Promise((resolve, reject) => {
            setInterval(() => {
                if (window.ApiClient != undefined) resolve(window.ApiClient);
            }, 16);
        });
        return await client.${func}(${arg})
        `;
        return this.injectCode(script);
    }

    static getItems(query) {
        //由于要合并多个媒体库所以放弃做缓存
        return this.injectCall("getItems", "client.getCurrentUserId(), " + JSON.stringify(query));
    }

    static itemsRandom(array){
        let res = [], random;
        while(array.length>0){
            random = Math.floor(Math.random()*array.length);
            res.push(array[random]);
            array.splice(random, 1);
        }
        return res;
    }

    static async getItem(itemId) {
        // 双缓存 优先使用 WebStorage
        if (typeof Storage !== "undefined" && !localStorage.getItem("CACHE|" + itemId) && !this.cache.item.has(itemId)) {
            const data = JSON.stringify(await this.injectCall("getItem", `client.getCurrentUserId(), "${itemId}"`));
            if (typeof Storage !== "undefined") localStorage.setItem("CACHE|" + itemId, data);
            else this.cache.item.set(itemId, data);
        }
        return JSON.parse(typeof Storage !== "undefined" ? localStorage.getItem("CACHE|" + itemId) : this.cache.item.get(itemId));
    }

    static getImageUrl(itemId, options) {
        return this.injectCall("getImageUrl", itemId + ", " + JSON.stringify(options));
    }

    static async appendItem(i){
        const detail = await this.getItem(this.data.Items[i].Id),
                itemHtml = `
            <div class="misty-banner-item" id="${detail.Id}">
                <img draggable="false" loading="eager" decoding="async" class="misty-banner-cover" src="${await this.getImageUrl(detail.Id, this.coverOptions)}" alt="Backdrop" style="">
                <div class="misty-banner-info padded-left padded-right">
                    <h1>${detail.Name}</h1>
                    <div><p>${detail.Overview}</p></div>
                    <div><button onclick="appRouter.showItem('${detail.Id}')">MORE</button></div>
                </div>
            </div>
            `,
                logoHtml = `
            <img id="${detail.Id}" draggable="false" loading="auto" decoding="lazy" class="misty-banner-logo" data-banner="img-title" alt="Logo" src="${await this.getImageUrl(detail.Id, this.logoOptions)}">
            `;
            if (detail.ImageTags && detail.ImageTags.Logo) {
                $(".misty-banner-logos").append(logoHtml);
            }
            $(".misty-banner-body").append(itemHtml);
    }

    static async bannerRoll(){
        // 背景切换
        this.index += this.index + 1 == $(".misty-banner-item").length ? -this.index : 1;
        //已经切换到最后且总数大于10
        if(this.index == 0 && this.data.Items.length >= 10){
            clearInterval(this.bannerInterval);
            let l = $(".misty-banner-item").length;
            //剩余小于10张直接舍弃,从头开始
            if(this.count+1==this.data.Items.length||this.data.Items.length-this.count-1<10)
                this.count=-1;
            //向后添加,剩余10张以上添加10张
            for(let i=this.count+1;i<this.count+1+(this.data.Items.length-this.count-1<10?this.data.Items.length-this.count-1:10);i++){
                await this.appendItem(i)
            }
            //切换到第一张
            $(".misty-banner-body").css("left", "0%");
            $(".misty-banner-item.active").removeClass("active");
            let id = $(".misty-banner-item").eq(l).addClass("active").attr("id");
            $(".misty-banner-logo.active").removeClass("active");
            $(`.misty-banner-logo[id=${id}]`).addClass("active");
            //从dom中移除上一轮次的横幅
            for(let i=0;i<l;i++){
                $(".misty-banner-item").eq(0).remove();
            }
            this.bannerInterval = setInterval(this.bannerRoll.bind(this), this.config.interval);
        } else {
            $(".misty-banner-body").css("left", -(this.index * 100).toString() + "%");
            // 信息切换
            $(".misty-banner-item.active").removeClass("active");
            let id = $(".misty-banner-item").eq(this.index).addClass("active").attr("id");
            // LOGO切换
            $(".misty-banner-logo.active").removeClass("active");
            $(`.misty-banner-logo[id=${id}]`).addClass("active");
        }
        this.count++;
    }

    /* 插入Banner */
    static async initBanner() {
        const banner = `
        <div class="misty-banner">
            <div class="misty-banner-body">
            </div>
            <div class="misty-banner-library">
                <div class="misty-banner-logos"></div>
            </div>
        </div>
        `;
        $(".view:not(.hide) .homeSectionsContainer").prepend(banner);
        $(".view:not(.hide) .section0").detach().appendTo(".view:not(.hide) .misty-banner-library");

        // 插入数据
        this.data = {Items:[]}
        //配置的媒体库不为空
        if(this.config.parentIds[0] != ""){
            //合并所有配置的媒体库的结果
            for(let parentId of this.config.parentIds){
                this.itemQuery.ParentId = parentId;
                let res = await this.getItems(this.itemQuery);
                this.data.Items = this.data.Items.concat(res.Items);
            }
        } else {
            //查询所有媒体库
            this.data = await this.getItems(this.itemQuery);
        }

        if(this.config.random==true){
            this.data.Items = this.itemsRandom(this.data.Items);
        }
        //大于10时添加10张                   
        for(let i=0;i<(this.data.Items.length<10?this.data.Items.length:10);i++){
            await this.appendItem(i)
        }

        // 只判断第一张海报加载完毕, 优化加载速度
        await new Promise((resolve, reject) => {
            let waitLoading = setInterval(() => {
                if (document.querySelector(".misty-banner-cover").complete) {
                    clearInterval(waitLoading);
                    resolve();
                }
            }, 16);
        });

        $(".misty-loading").fadeOut(500, () => $(".misty-loading").remove());
        await CommonUtils.sleep(150);
        // 置入场动画
        let delay = 80; // 动媒体库画间隔
        let id = $(".misty-banner-item").eq(0).addClass("active").attr("id"); // 初次信息动画
        $(`.misty-banner-logo[id=${id}]`).addClass("active");

        await CommonUtils.sleep(200); // 间隔动画
        $(".section0 > div").addClass("misty-banner-library-overflow"); // 关闭overflow 防止媒体库动画溢出
        $(".misty-banner .card").each((i, dom) => setTimeout(() => $(dom).addClass("misty-banner-library-show"), i * delay)); // 媒体库动画
        await CommonUtils.sleep(delay * 8 + 1000); // 等待媒体库动画完毕
        $(".section0 > div").removeClass("misty-banner-library-overflow"); // 开启overflow 防止无法滚动

        // 滚屏逻辑
        this.index = 0;this.count = 0;
        clearInterval(this.bannerInterval);
        this.bannerInterval = setInterval(this.bannerRoll.bind(this), this.config.interval);
    }

    /* 初始事件 */
    static initEvent() {
        // 通过注入方式, 方可调用appRouter函数, 以解决Content-Script window对象不同步问题
        const script = `
        // 挂载appRouter
        if (!window.appRouter) window.appRouter = (await window.require(["appRouter"]))[0];
        // 修复library事件参数
        const serverId = ApiClient._serverInfo.Id,
            librarys = document.querySelectorAll(".view:not(.hide) .section0 .card");
        librarys.forEach(library => {
            library.setAttribute("data-serverid", serverId);
            library.setAttribute("data-type", "CollectionFolder");
        });
        `;
        this.injectCode(script);
    }
}

// 运行
if ("BroadcastChannel" in window || "postMessage" in window) {
    if ($("meta[name=application-name]").attr("content") == "Emby" || $(".accent-emby") != undefined) {
        Home.start();
    }
}

非常感谢大佬,现在要是再适配手机版本的,真的就非常完美了, 在电报群里找到了一个适配手机的,但是MORE被修改成了PLAY,原版是进入详情,修改成了直接播放,对于使用外置播放器的,不是很友好。 那个也可以指定媒体库展示,但是不能选择多个媒体库,好像图片也是不是随机刷新的。 要是合并起来,大佬的可配置性强,电报大佬的手机适配好,绝了。 非常感谢原作者提供这个好的项目。

不懂的代码,拿来问GPT就行了,而且大佬们还给了中文注释

mobclix commented 1 year ago

大佬,您好,用这个后,是可以选择展示的媒体库了,但是不知道为什么,主页会生成两个展示大图,就是鼠标向下拉后,会有额外的一个一模一样的展示图,下方也带有一模一样的媒体库,都能点击进去,而且主页大图在轮换的时候会重复轮回一次,然后才切换到下一张,这个需要修改什么参数吗? 配置只改了this.parentId = "5";,其他未动。也未设置其他的CSS。

该issue提供的配置与项目本身应该是不同步的,你的这个问题在后者中已修复,参考后者进行修改

感谢回复,使用最新版的是解决了两个主页的问题,不过新的问题是,又不能自由的选择媒体库了,是不是大佬的config.js和作者大佬的main.js是不兼容的。

已合并项目最新更新

main.js

class Home {
    static start() {
        this.cache = {
            items: undefined,
            item: new Map(),
        };
        this.config = new Config();
        this.itemQuery = this.config.itemQuery;
        this.coverOptions = { type: "Backdrop", maxWidth: 3000 };
        this.logoOptions = { type: "Logo", maxWidth: 3000 };
        this.initStart = false;
        setInterval(() => {
            if (window.location.href.indexOf("!/home") != -1) {
                if ($(".view:not(.hide) .misty-banner").length == 0 && $(".misty-loading").length == 0) {
                    this.initStart = false;
                    this.initLoading();
                }
                if ($(".hide .misty-banner").length != 0) {
                    $(".hide .misty-banner").remove();
                }
                if (!this.initStart && $(".section0 .card").length != 0 && $(".view:not(.hide) .misty-banner").length == 0) {
                    this.initStart = true;
                    this.init();
                }
            }
        }, 100);
    }

    static async init() {
        // Beta
        $(".view:not(.hide)").attr("data-type", "home");
        // Loading
        const serverName = await this.injectCall("serverName", "");
        $(".misty-loading h1").text(serverName).addClass("active");
        // Banner
        await this.initBanner();
        this.initEvent();
    }

    /* 插入Loading */
    static initLoading() {
        const load = `
        <div class="misty-loading">
            <h1></h1>
            <div class="mdl-spinner"><div class="mdl-spinner__layer mdl-spinner__layer-1"><div class="mdl-spinner__circle-clipper mdl-spinner__left"><div class="mdl-spinner__circle mdl-spinner__circleLeft"></div></div><div class="mdl-spinner__circle-clipper mdl-spinner__right"><div class="mdl-spinner__circle mdl-spinner__circleRight"></div></div></div></div>
        </div>
        `;
        $("body").append(load);
    }

    static injectCode(code) {
        let hash = md5(code + Math.random().toString());
        return new Promise((resolve, reject) => {
            if ("BroadcastChannel" in window) {
                const channel = new BroadcastChannel(hash);
                channel.addEventListener("message", (event) => resolve(event.data));
            } else if ("postMessage" in window) {
                window.addEventListener("message", (event) => {
                    if (event.data.channel === hash) {
                        resolve(event.data.message);
                    }
                });
            }
            const script = `
            <script class="I${hash}">
                setTimeout(async ()=> {
                    async function R${hash}(){${code}};
                    if ("BroadcastChannel" in window) {
                        const channel = new BroadcastChannel("${hash}");
                        channel.postMessage(await R${hash}());
                    } else if ('postMessage' in window) {
                        window.parent.postMessage({channel:"${hash}",message:await R${hash}()}, "*");
                    }
                    document.querySelector("script.I${hash}").remove()
                }, 16)
            </script>
            `;
            $(document.head || document.documentElement).append(script);
        });
    }

    static injectCall(func, arg) {
        const script = `
        // const client = (await window.require(["ApiClient"]))[0];
        const client = await new Promise((resolve, reject) => {
            setInterval(() => {
                if (window.ApiClient != undefined) resolve(window.ApiClient);
            }, 16);
        });
        return await client.${func}(${arg})
        `;
        return this.injectCode(script);
    }

    static getItems(query) {
        //由于要合并多个媒体库所以放弃做缓存
        return this.injectCall("getItems", "client.getCurrentUserId(), " + JSON.stringify(query));
    }

    static itemsRandom(array){
        let res = [], random;
        while(array.length>0){
            random = Math.floor(Math.random()*array.length);
            res.push(array[random]);
            array.splice(random, 1);
        }
        return res;
    }

    static async getItem(itemId) {
        // 双缓存 优先使用 WebStorage
        if (typeof Storage !== "undefined" && !localStorage.getItem("CACHE|" + itemId) && !this.cache.item.has(itemId)) {
            const data = JSON.stringify(await this.injectCall("getItem", `client.getCurrentUserId(), "${itemId}"`));
            if (typeof Storage !== "undefined") localStorage.setItem("CACHE|" + itemId, data);
            else this.cache.item.set(itemId, data);
        }
        return JSON.parse(typeof Storage !== "undefined" ? localStorage.getItem("CACHE|" + itemId) : this.cache.item.get(itemId));
    }

    static getImageUrl(itemId, options) {
        return this.injectCall("getImageUrl", itemId + ", " + JSON.stringify(options));
    }

    static async appendItem(i){
        const detail = await this.getItem(this.data.Items[i].Id),
                itemHtml = `
            <div class="misty-banner-item" id="${detail.Id}">
                <img draggable="false" loading="eager" decoding="async" class="misty-banner-cover" src="${await this.getImageUrl(detail.Id, this.coverOptions)}" alt="Backdrop" style="">
                <div class="misty-banner-info padded-left padded-right">
                    <h1>${detail.Name}</h1>
                    <div><p>${detail.Overview}</p></div>
                    <div><button onclick="appRouter.showItem('${detail.Id}')">MORE</button></div>
                </div>
            </div>
            `,
                logoHtml = `
            <img id="${detail.Id}" draggable="false" loading="auto" decoding="lazy" class="misty-banner-logo" data-banner="img-title" alt="Logo" src="${await this.getImageUrl(detail.Id, this.logoOptions)}">
            `;
            if (detail.ImageTags && detail.ImageTags.Logo) {
                $(".misty-banner-logos").append(logoHtml);
            }
            $(".misty-banner-body").append(itemHtml);
    }

    static async bannerRoll(){
        // 背景切换
        this.index += this.index + 1 == $(".misty-banner-item").length ? -this.index : 1;
        //已经切换到最后且总数大于10
        if(this.index == 0 && this.data.Items.length >= 10){
            clearInterval(this.bannerInterval);
            let l = $(".misty-banner-item").length;
            //剩余小于10张直接舍弃,从头开始
            if(this.count+1==this.data.Items.length||this.data.Items.length-this.count-1<10)
                this.count=-1;
            //向后添加,剩余10张以上添加10张
            for(let i=this.count+1;i<this.count+1+(this.data.Items.length-this.count-1<10?this.data.Items.length-this.count-1:10);i++){
                await this.appendItem(i)
            }
            //切换到第一张
            $(".misty-banner-body").css("left", "0%");
            $(".misty-banner-item.active").removeClass("active");
            let id = $(".misty-banner-item").eq(l).addClass("active").attr("id");
            $(".misty-banner-logo.active").removeClass("active");
            $(`.misty-banner-logo[id=${id}]`).addClass("active");
            //从dom中移除上一轮次的横幅
            for(let i=0;i<l;i++){
                $(".misty-banner-item").eq(0).remove();
            }
            this.bannerInterval = setInterval(this.bannerRoll.bind(this), this.config.interval);
        } else {
            $(".misty-banner-body").css("left", -(this.index * 100).toString() + "%");
            // 信息切换
            $(".misty-banner-item.active").removeClass("active");
            let id = $(".misty-banner-item").eq(this.index).addClass("active").attr("id");
            // LOGO切换
            $(".misty-banner-logo.active").removeClass("active");
            $(`.misty-banner-logo[id=${id}]`).addClass("active");
        }
        this.count++;
    }

    /* 插入Banner */
    static async initBanner() {
        const banner = `
        <div class="misty-banner">
            <div class="misty-banner-body">
            </div>
            <div class="misty-banner-library">
                <div class="misty-banner-logos"></div>
            </div>
        </div>
        `;
        $(".view:not(.hide) .homeSectionsContainer").prepend(banner);
        $(".view:not(.hide) .section0").detach().appendTo(".view:not(.hide) .misty-banner-library");

        // 插入数据
        this.data = {Items:[]}
        //配置的媒体库不为空
        if(this.config.parentIds[0] != ""){
            //合并所有配置的媒体库的结果
            for(let parentId of this.config.parentIds){
                this.itemQuery.ParentId = parentId;
                let res = await this.getItems(this.itemQuery);
                this.data.Items = this.data.Items.concat(res.Items);
            }
        } else {
            //查询所有媒体库
            this.data = await this.getItems(this.itemQuery);
        }

        if(this.config.random==true){
            this.data.Items = this.itemsRandom(this.data.Items);
        }
        //大于10时添加10张                   
        for(let i=0;i<(this.data.Items.length<10?this.data.Items.length:10);i++){
            await this.appendItem(i)
        }

        // 只判断第一张海报加载完毕, 优化加载速度
        await new Promise((resolve, reject) => {
            let waitLoading = setInterval(() => {
                if (document.querySelector(".misty-banner-cover").complete) {
                    clearInterval(waitLoading);
                    resolve();
                }
            }, 16);
        });

        $(".misty-loading").fadeOut(500, () => $(".misty-loading").remove());
        await CommonUtils.sleep(150);
        // 置入场动画
        let delay = 80; // 动媒体库画间隔
        let id = $(".misty-banner-item").eq(0).addClass("active").attr("id"); // 初次信息动画
        $(`.misty-banner-logo[id=${id}]`).addClass("active");

        await CommonUtils.sleep(200); // 间隔动画
        $(".section0 > div").addClass("misty-banner-library-overflow"); // 关闭overflow 防止媒体库动画溢出
        $(".misty-banner .card").each((i, dom) => setTimeout(() => $(dom).addClass("misty-banner-library-show"), i * delay)); // 媒体库动画
        await CommonUtils.sleep(delay * 8 + 1000); // 等待媒体库动画完毕
        $(".section0 > div").removeClass("misty-banner-library-overflow"); // 开启overflow 防止无法滚动

        // 滚屏逻辑
        this.index = 0;this.count = 0;
        clearInterval(this.bannerInterval);
        this.bannerInterval = setInterval(this.bannerRoll.bind(this), this.config.interval);
    }

    /* 初始事件 */
    static initEvent() {
        // 通过注入方式, 方可调用appRouter函数, 以解决Content-Script window对象不同步问题
        const script = `
        // 挂载appRouter
        if (!window.appRouter) window.appRouter = (await window.require(["appRouter"]))[0];
        // 修复library事件参数
        const serverId = ApiClient._serverInfo.Id,
            librarys = document.querySelectorAll(".view:not(.hide) .section0 .card");
        librarys.forEach(library => {
            library.setAttribute("data-serverid", serverId);
            library.setAttribute("data-type", "CollectionFolder");
        });
        `;
        this.injectCode(script);
    }
}

// 运行
if ("BroadcastChannel" in window || "postMessage" in window) {
    if ($("meta[name=application-name]").attr("content") == "Emby" || $(".accent-emby") != undefined) {
        Home.start();
    }
}

非常感谢大佬,现在要是再适配手机版本的,真的就非常完美了, 在电报群里找到了一个适配手机的,但是MORE被修改成了PLAY,原版是进入详情,修改成了直接播放,对于使用外置播放器的,不是很友好。 那个也可以指定媒体库展示,但是不能选择多个媒体库,好像图片也是不是随机刷新的。 要是合并起来,大佬的可配置性强,电报大佬的手机适配好,绝了。 非常感谢原作者提供这个好的项目。

这个好说,在main.js里找到 this.coverOptions = { type: "Backdrop", maxWidth: 3000 }; 后面加上

setInterval(() => {
            //如果高度大于宽度,判断为竖屏
            if($(".mainAnimatedPages").width()<$(".mainAnimatedPages").height()){
                //如果横竖屏发生切换,直接开始新一轮次
                if(this.coverOptions.type != "Primary")
                        this.index = -1;
                this.coverOptions = { type: "Primary", maxWidth: 3000 };
                //logo大小
                $(".misty-banner-logo").css("height","clamp(0rem, -2.182rem + 18.91vw, 6rem)");
                //logo位置
                $(".misty-banner-logos").css("margin-bottom","3em");
                //标题字体大小
                $(".misty-banner-info h1").css("font-size","clamp(2rem, -.362rem + 4.75vw, 4.7rem)");
                //简介字体大小
                $(".misty-banner-info p").css("font-size","clamp(.6rem, .4rem + 1.6vw, 1.6rem)");
                //简介行数
                $(".misty-banner-info p").css("-webkit-line-clamp","4");
            } else {//横屏
                if(this.coverOptions.type != "Backdrop")
                        this.index = -1;
                this.coverOptions = { type: "Backdrop", maxWidth: 3000 };
                $(".misty-banner-logo").css("height","clamp(0rem, -2.182rem + 10.91vw, 6rem)");
                $(".misty-banner-logos").css("margin-bottom","0");
                $(".misty-banner-info h1").css("font-size","clamp(2rem, -.362rem + 6.75vw, 4.7rem)");
                $(".misty-banner-info p").css("font-size","clamp(.6rem, .4rem + 1vw, 1.6rem)");
                $(".misty-banner-info p").css("-webkit-line-clamp","2");
            }
        },1000);

然后在style.css里找到

@media screen and (max-width: 62.5em) {
    .misty-banner-info button {
        /*margin-top: 0;*/
    }
}
@media screen and (max-width: 52em) {
    .misty-banner-info button,
    .misty-banner-info p {
        /*display: none;*/
    }
}

@media screen and (max-width: 35em) {
    .misty-banner-info,
    .misty-banner-logo  {
        /*display: none !important;*/
    }
    .misty-banner-body{
        /*opacity: 0;*/
    }
}

把这些都注释掉就行了

可以适配手机竖屏,如果样式有问题就根据自己设备修改一下

没用过电报那个不知道效果一不一样

yue631377105 commented 1 year ago

大佬,您好,用这个后,是可以选择展示的媒体库了,但是不知道为什么,主页会生成两个展示大图,就是鼠标向下拉后,会有额外的一个一模一样的展示图,下方也带有一模一样的媒体库,都能点击进去,而且主页大图在轮换的时候会重复轮回一次,然后才切换到下一张,这个需要修改什么参数吗? 配置只改了this.parentId = "5";,其他未动。也未设置其他的CSS。

该issue提供的配置与项目本身应该是不同步的,你的这个问题在后者中已修复,参考后者进行修改

感谢回复,使用最新版的是解决了两个主页的问题,不过新的问题是,又不能自由的选择媒体库了,是不是大佬的config.js和作者大佬的main.js是不兼容的。

已合并项目最新更新

main.js

class Home {
  static start() {
      this.cache = {
          items: undefined,
          item: new Map(),
      };
      this.config = new Config();
      this.itemQuery = this.config.itemQuery;
      this.coverOptions = { type: "Backdrop", maxWidth: 3000 };
      this.logoOptions = { type: "Logo", maxWidth: 3000 };
      this.initStart = false;
      setInterval(() => {
          if (window.location.href.indexOf("!/home") != -1) {
              if ($(".view:not(.hide) .misty-banner").length == 0 && $(".misty-loading").length == 0) {
                  this.initStart = false;
                  this.initLoading();
              }
              if ($(".hide .misty-banner").length != 0) {
                  $(".hide .misty-banner").remove();
              }
              if (!this.initStart && $(".section0 .card").length != 0 && $(".view:not(.hide) .misty-banner").length == 0) {
                  this.initStart = true;
                  this.init();
              }
          }
      }, 100);
  }

  static async init() {
      // Beta
      $(".view:not(.hide)").attr("data-type", "home");
      // Loading
      const serverName = await this.injectCall("serverName", "");
      $(".misty-loading h1").text(serverName).addClass("active");
      // Banner
      await this.initBanner();
      this.initEvent();
  }

  /* 插入Loading */
  static initLoading() {
      const load = `
      <div class="misty-loading">
          <h1></h1>
          <div class="mdl-spinner"><div class="mdl-spinner__layer mdl-spinner__layer-1"><div class="mdl-spinner__circle-clipper mdl-spinner__left"><div class="mdl-spinner__circle mdl-spinner__circleLeft"></div></div><div class="mdl-spinner__circle-clipper mdl-spinner__right"><div class="mdl-spinner__circle mdl-spinner__circleRight"></div></div></div></div>
      </div>
      `;
      $("body").append(load);
  }

  static injectCode(code) {
      let hash = md5(code + Math.random().toString());
      return new Promise((resolve, reject) => {
          if ("BroadcastChannel" in window) {
              const channel = new BroadcastChannel(hash);
              channel.addEventListener("message", (event) => resolve(event.data));
          } else if ("postMessage" in window) {
              window.addEventListener("message", (event) => {
                  if (event.data.channel === hash) {
                      resolve(event.data.message);
                  }
              });
          }
          const script = `
          <script class="I${hash}">
              setTimeout(async ()=> {
                  async function R${hash}(){${code}};
                  if ("BroadcastChannel" in window) {
                      const channel = new BroadcastChannel("${hash}");
                      channel.postMessage(await R${hash}());
                  } else if ('postMessage' in window) {
                      window.parent.postMessage({channel:"${hash}",message:await R${hash}()}, "*");
                  }
                  document.querySelector("script.I${hash}").remove()
              }, 16)
          </script>
          `;
          $(document.head || document.documentElement).append(script);
      });
  }

  static injectCall(func, arg) {
      const script = `
      // const client = (await window.require(["ApiClient"]))[0];
      const client = await new Promise((resolve, reject) => {
          setInterval(() => {
              if (window.ApiClient != undefined) resolve(window.ApiClient);
          }, 16);
      });
      return await client.${func}(${arg})
      `;
      return this.injectCode(script);
  }

  static getItems(query) {
      //由于要合并多个媒体库所以放弃做缓存
      return this.injectCall("getItems", "client.getCurrentUserId(), " + JSON.stringify(query));
  }

  static itemsRandom(array){
      let res = [], random;
      while(array.length>0){
          random = Math.floor(Math.random()*array.length);
          res.push(array[random]);
          array.splice(random, 1);
      }
      return res;
  }

  static async getItem(itemId) {
      // 双缓存 优先使用 WebStorage
      if (typeof Storage !== "undefined" && !localStorage.getItem("CACHE|" + itemId) && !this.cache.item.has(itemId)) {
          const data = JSON.stringify(await this.injectCall("getItem", `client.getCurrentUserId(), "${itemId}"`));
          if (typeof Storage !== "undefined") localStorage.setItem("CACHE|" + itemId, data);
          else this.cache.item.set(itemId, data);
      }
      return JSON.parse(typeof Storage !== "undefined" ? localStorage.getItem("CACHE|" + itemId) : this.cache.item.get(itemId));
  }

  static getImageUrl(itemId, options) {
      return this.injectCall("getImageUrl", itemId + ", " + JSON.stringify(options));
  }

  static async appendItem(i){
      const detail = await this.getItem(this.data.Items[i].Id),
              itemHtml = `
          <div class="misty-banner-item" id="${detail.Id}">
              <img draggable="false" loading="eager" decoding="async" class="misty-banner-cover" src="${await this.getImageUrl(detail.Id, this.coverOptions)}" alt="Backdrop" style="">
              <div class="misty-banner-info padded-left padded-right">
                  <h1>${detail.Name}</h1>
                  <div><p>${detail.Overview}</p></div>
                  <div><button onclick="appRouter.showItem('${detail.Id}')">MORE</button></div>
              </div>
          </div>
          `,
              logoHtml = `
          <img id="${detail.Id}" draggable="false" loading="auto" decoding="lazy" class="misty-banner-logo" data-banner="img-title" alt="Logo" src="${await this.getImageUrl(detail.Id, this.logoOptions)}">
          `;
          if (detail.ImageTags && detail.ImageTags.Logo) {
              $(".misty-banner-logos").append(logoHtml);
          }
          $(".misty-banner-body").append(itemHtml);
  }

  static async bannerRoll(){
      // 背景切换
      this.index += this.index + 1 == $(".misty-banner-item").length ? -this.index : 1;
      //已经切换到最后且总数大于10
      if(this.index == 0 && this.data.Items.length >= 10){
          clearInterval(this.bannerInterval);
          let l = $(".misty-banner-item").length;
          //剩余小于10张直接舍弃,从头开始
          if(this.count+1==this.data.Items.length||this.data.Items.length-this.count-1<10)
              this.count=-1;
          //向后添加,剩余10张以上添加10张
          for(let i=this.count+1;i<this.count+1+(this.data.Items.length-this.count-1<10?this.data.Items.length-this.count-1:10);i++){
              await this.appendItem(i)
          }
          //切换到第一张
          $(".misty-banner-body").css("left", "0%");
          $(".misty-banner-item.active").removeClass("active");
          let id = $(".misty-banner-item").eq(l).addClass("active").attr("id");
          $(".misty-banner-logo.active").removeClass("active");
          $(`.misty-banner-logo[id=${id}]`).addClass("active");
          //从dom中移除上一轮次的横幅
          for(let i=0;i<l;i++){
              $(".misty-banner-item").eq(0).remove();
          }
          this.bannerInterval = setInterval(this.bannerRoll.bind(this), this.config.interval);
      } else {
          $(".misty-banner-body").css("left", -(this.index * 100).toString() + "%");
          // 信息切换
          $(".misty-banner-item.active").removeClass("active");
          let id = $(".misty-banner-item").eq(this.index).addClass("active").attr("id");
          // LOGO切换
          $(".misty-banner-logo.active").removeClass("active");
          $(`.misty-banner-logo[id=${id}]`).addClass("active");
      }
      this.count++;
  }

  /* 插入Banner */
  static async initBanner() {
      const banner = `
      <div class="misty-banner">
          <div class="misty-banner-body">
          </div>
          <div class="misty-banner-library">
              <div class="misty-banner-logos"></div>
          </div>
      </div>
      `;
      $(".view:not(.hide) .homeSectionsContainer").prepend(banner);
      $(".view:not(.hide) .section0").detach().appendTo(".view:not(.hide) .misty-banner-library");

      // 插入数据
      this.data = {Items:[]}
      //配置的媒体库不为空
      if(this.config.parentIds[0] != ""){
          //合并所有配置的媒体库的结果
          for(let parentId of this.config.parentIds){
              this.itemQuery.ParentId = parentId;
              let res = await this.getItems(this.itemQuery);
              this.data.Items = this.data.Items.concat(res.Items);
          }
      } else {
          //查询所有媒体库
          this.data = await this.getItems(this.itemQuery);
      }

      if(this.config.random==true){
          this.data.Items = this.itemsRandom(this.data.Items);
      }
      //大于10时添加10张                   
      for(let i=0;i<(this.data.Items.length<10?this.data.Items.length:10);i++){
          await this.appendItem(i)
      }

      // 只判断第一张海报加载完毕, 优化加载速度
      await new Promise((resolve, reject) => {
          let waitLoading = setInterval(() => {
              if (document.querySelector(".misty-banner-cover").complete) {
                  clearInterval(waitLoading);
                  resolve();
              }
          }, 16);
      });

      $(".misty-loading").fadeOut(500, () => $(".misty-loading").remove());
      await CommonUtils.sleep(150);
      // 置入场动画
      let delay = 80; // 动媒体库画间隔
      let id = $(".misty-banner-item").eq(0).addClass("active").attr("id"); // 初次信息动画
      $(`.misty-banner-logo[id=${id}]`).addClass("active");

      await CommonUtils.sleep(200); // 间隔动画
      $(".section0 > div").addClass("misty-banner-library-overflow"); // 关闭overflow 防止媒体库动画溢出
      $(".misty-banner .card").each((i, dom) => setTimeout(() => $(dom).addClass("misty-banner-library-show"), i * delay)); // 媒体库动画
      await CommonUtils.sleep(delay * 8 + 1000); // 等待媒体库动画完毕
      $(".section0 > div").removeClass("misty-banner-library-overflow"); // 开启overflow 防止无法滚动

      // 滚屏逻辑
      this.index = 0;this.count = 0;
      clearInterval(this.bannerInterval);
      this.bannerInterval = setInterval(this.bannerRoll.bind(this), this.config.interval);
  }

  /* 初始事件 */
  static initEvent() {
      // 通过注入方式, 方可调用appRouter函数, 以解决Content-Script window对象不同步问题
      const script = `
      // 挂载appRouter
      if (!window.appRouter) window.appRouter = (await window.require(["appRouter"]))[0];
      // 修复library事件参数
      const serverId = ApiClient._serverInfo.Id,
          librarys = document.querySelectorAll(".view:not(.hide) .section0 .card");
      librarys.forEach(library => {
          library.setAttribute("data-serverid", serverId);
          library.setAttribute("data-type", "CollectionFolder");
      });
      `;
      this.injectCode(script);
  }
}

// 运行
if ("BroadcastChannel" in window || "postMessage" in window) {
  if ($("meta[name=application-name]").attr("content") == "Emby" || $(".accent-emby") != undefined) {
      Home.start();
  }
}

非常感谢大佬,现在要是再适配手机版本的,真的就非常完美了, 在电报群里找到了一个适配手机的,但是MORE被修改成了PLAY,原版是进入详情,修改成了直接播放,对于使用外置播放器的,不是很友好。 那个也可以指定媒体库展示,但是不能选择多个媒体库,好像图片也是不是随机刷新的。 要是合并起来,大佬的可配置性强,电报大佬的手机适配好,绝了。 非常感谢原作者提供这个好的项目。

这个好说,在main.js里找到 this.coverOptions = { type: "Backdrop", maxWidth: 3000 }; 后面加上

setInterval(() => {
          //如果高度大于宽度,判断为竖屏
          if($(".mainAnimatedPages").width()<$(".mainAnimatedPages").height()){
              //如果横竖屏发生切换,直接开始新一轮次
              if(this.coverOptions.type != "Primary")
                      this.index = -1;
              this.coverOptions = { type: "Primary", maxWidth: 3000 };
              //logo大小
              $(".misty-banner-logo").css("height","clamp(0rem, -2.182rem + 18.91vw, 6rem)");
              //logo位置
              $(".misty-banner-logos").css("margin-bottom","3em");
              //标题字体大小
              $(".misty-banner-info h1").css("font-size","clamp(2rem, -.362rem + 4.75vw, 4.7rem)");
              //简介字体大小
              $(".misty-banner-info p").css("font-size","clamp(.6rem, .4rem + 1.6vw, 1.6rem)");
              //简介行数
              $(".misty-banner-info p").css("-webkit-line-clamp","4");
          } else {//横屏
              if(this.coverOptions.type != "Backdrop")
                      this.index = -1;
              this.coverOptions = { type: "Backdrop", maxWidth: 3000 };
              $(".misty-banner-logo").css("height","clamp(0rem, -2.182rem + 10.91vw, 6rem)");
              $(".misty-banner-logos").css("margin-bottom","0");
              $(".misty-banner-info h1").css("font-size","clamp(2rem, -.362rem + 6.75vw, 4.7rem)");
              $(".misty-banner-info p").css("font-size","clamp(.6rem, .4rem + 1vw, 1.6rem)");
              $(".misty-banner-info p").css("-webkit-line-clamp","2");
          }
      },1000);

然后在style.css里找到

@media screen and (max-width: 62.5em) {
  .misty-banner-info button {
      /*margin-top: 0;*/
  }
}
@media screen and (max-width: 52em) {
  .misty-banner-info button,
  .misty-banner-info p {
      /*display: none;*/
  }
}

@media screen and (max-width: 35em) {
  .misty-banner-info,
  .misty-banner-logo  {
      /*display: none !important;*/
  }
  .misty-banner-body{
      /*opacity: 0;*/
  }
}

把这些都注释掉就行了

可以适配手机竖屏,如果样式有问题就根据自己设备修改一下

没用过电报那个不知道效果一不一样

感谢大佬,可以了。很棒的效果,谢谢!这回完美了。

aprilowolovejiuwang commented 1 year ago

大佬你好,我想请问下,你提的pr,非首页禁用轮播把代码放在哪个位置哇😭,我小白不太会,求赐教

mobclix commented 1 year ago

大佬你好,我想请问下,你提的pr,非首页禁用轮播把代码放在哪个位置哇😭,我小白不太会,求赐教


        //总数小于10
    static bannerRollOnce(){
                //非活跃状态停止轮播
        if(!window.location.href.endsWith("home")||document.hidden)
            return;
        this.index += this.index + 1 == $(".misty-banner-item").length ? -this.index : 1;
        /*todo*/
    }

    //总数大于10
    static async bannerRoll(){
        //非活跃状态停止轮播
        if(!window.location.href.endsWith("home")||document.hidden)
            return;
                // 背景切换
        this.index += this.index + 1 == $(".misty-banner-item").length ? -this.index : 1;
                /*todo*/
        }
aprilowolovejiuwang commented 1 year ago

大佬你好,我想请问下,你提的pr,非首页禁用轮播把代码放在哪个位置哇😭,我小白不太会,求赐教

        //总数小于10
  static bannerRollOnce(){
                //非活跃状态停止轮播
      if(!window.location.href.endsWith("home")||document.hidden)
          return;
      this.index += this.index + 1 == $(".misty-banner-item").length ? -this.index : 1;
      /*todo*/
  }

  //总数大于10
  static async bannerRoll(){
      //非活跃状态停止轮播
      if(!window.location.href.endsWith("home")||document.hidden)
          return;
                // 背景切换
      this.index += this.index + 1 == $(".misty-banner-item").length ? -this.index : 1;
                /*todo*/
        }

谢谢大佬,完这下完美了

xrbzy64 commented 1 year ago

大佬你好,能否修改标题、简介的字体和颜色?

guvmao commented 1 year ago

@mobclix 可否在你的 Fork 中使用新的分支提交您的更新?

mobclix commented 1 year ago

大佬你好,能否修改标题、简介的字体和颜色? style.css


.misty-banner-info h1 {
/*标题颜色*/
color: #fff !important;
/*字体*/
font-family: 宋体;
font-size: clamp(2rem, -.362rem + 6.75vw, 4.7rem);
font-weight: bolder;
margin: 0;
text-shadow: 0 4px 10px rgb(0 0 0 / 20%);
/* margin-bottom: clamp(0rem, -.545rem + 2.73vw, 1.5rem); */
max-width: 80%;
display: -webkit-box !important;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
}

.misty-banner-info p { /简介颜色/ color: #fff !important; /字体/ font-family: 宋体; font-size: clamp(.6rem, .4rem + 1vw, 1.6rem); font-weight: bold; max-width: 47%; opacity: .7; / overflow: hidden; white-space: nowrap; text-overflow: ellipsis; / display: -webkit-box !important; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }