打开/关闭菜单
打开/关闭外观设置菜单
打开/关闭个人菜单
未登录
未登录用户的IP地址会在进行任意编辑后公开展示。

Module:VOCALOID Songbox/new

来自Vocawiki
Lower留言 | 贡献2025年10月23日 (四) 15:51的版本 (// Edit via InPageEdit)

此模块的文档可以在Module:VOCALOID Songbox/new/doc创建

local strings = require('Module:Strings')
local get = require('Module:Get')
local acandy = require('Module:ACandy')
local a, Fragment, Raw = acandy.a, acandy.Fragment, acandy.Raw
local div, span = a.div, a.span
local trim = mw.text.trim

---@alias Image {
---	file: string,
---	caption: string?,
---	size: string?,
---}

---@alias Date {
--- year: number,
--- month: number,
--- day: number,
---}

---@alias Platform { id: string, date: Date }

---@alias Platforms {
---	nico: Platform?,
---	bili: Platform?,
---	acfun: Platform?,
---	ncm: Platform?,
---	ytb: Platform?,
---}

---@alias Style {
---	general: string?,
---	[1]: string?,
---	[2]: string?,
---	[3]: string?,
---}

---@param str string?
---@return string?
local function clean(str)
	if not str then return nil end
	str = trim(str)
	return str ~= '' and str or nil
end

---@param s1 string?
---@param s2 string?
---@return string?
local function merge(s1, s2)
	local merged = trim((s1 or '')..(s2 or ''))
	return merged ~= '' and merged or nil
end

local function raw_or_empty(v)
	return v ~= nil and Raw(v) or ''
end


local c = {}  -- components

---@param props {
---	image: Image?,
---	tabs: string?,
---	title: string?,
---	uploader: string?,
---	producer: string?,
---	note: string?,
---	singer: string?,
---	albums: string?,
---	platforms: Platforms,
---	submissions: string?,
---	colors: Style,
---	text_styles: Style,
---}
function c.SongBox(props, frame)
	local colors = props.colors
	local text_styles = props.text_styles

	local function THeader(nth, content)
		local th_style = colors[nth] or colors.general
		if th_style then
			th_style = 'background:'..th_style
		else
			th_style = 'background: #66CDAA; color: #FFF;'
		end
		return div['songbox_th'] {
			style = th_style,
			span {
				style = 'display: inline-block;'..(text_styles[nth] or text_styles.general or ''),
				content,
			},
		}
	end

	return div['songbox'] {
		props.image and c.Image(props.image) or '',
		raw_or_empty(props.tabs),
		c.Title(props.title),
		c.Note(props),
		THeader(1, '演唱'),
		div['songbox_td'] / raw_or_empty(props.singer),
		THeader(2, props.uploader and 'UP主' or 'P主'),
		div['songbox_td'] / raw_or_empty(props.uploader or props.producer),
		THeader(3, props.albums and '收录专辑' or '投稿'),
		div['songbox_td'] / div['songbox_submission_container']
		 / (props.albums
		 	and Raw(props.albums)
		 	or { c.Submissions(props.platforms), raw_or_empty(props.submissions) }
		 ),
	}
end

---@param props Image
function c.Image(props)
	local frame = mw.getCurrentFrame()
	return Raw(frame:expandTemplate { title = '氛围图片', args = props })
end

---@param title string?
function c.Title(title)
	if not title then
		return div['songbox_titles unknown']
	end

	local titles = strings.split(title, '%s*<br%s*/?>%s*')
	local main_title = table.remove(titles, 1)
	return div['songbox_titles'] {
		div['songbox_main-title'] / Raw(main_title),
		Raw(table.concat(titles, '<br>')),
	}
end

---@param props {note: string?}
function c.Note(props)
	return props.note and div['songbox_note'] {
		raw_or_empty(props.note),
	} or ''
end

local platform_data = get(
	{ key = 'nico',  url = 'https://www.nicovideo.jp/watch/%s',  name = 'niconico' },
	{ key = 'bili',  url = 'https://www.bilibili.com/video/%s',  name = 'bilibili' },
	{ key = 'acfun', url = 'http://www.acfun.cn/v/%s',           name = 'AcFun'    },
	{ key = 'ncm',   url = 'https://music.163.com/#/song?id=%s', name = '网易云'   },
	{ key = 'ytb',   url = 'https://www.youtube.com/watch?v=%s', name = 'YouTube'  })

---@param props Platforms
function c.Submissions(props)
	local submissions = Fragment()
	for platform in platform_data:filter(function (x) return props[x.key] end) do
		---@cast platform { key: string, url: string, name: string }
		local data = props[platform.key]  ---@cast data Platform
		submissions:insert(c.Card(platform, data))
	end
	return submissions
end

---@param platform { key: string, url: string, name: string }
---@param data Platform
function c.Card(platform, data, overrides)
	overrides = overrides or {}

	local frame = mw.getCurrentFrame()
	local card = div[('songbox_submission %s %s'):format(platform.key, overrides.class or '')] {
		div['submission_link']
			/ (data.id and Raw(('[%s -{}-]'):format(platform.url:format(data.id)))),
		div['submission_platform'] / Raw(overrides.title or platform.name),
		div['submission_info'] {
			div['submission_date']
				/ Raw(('%s-%s-%s'):format(data.date.year, data.date.month, data.date.day)),
			div['submission_playcount'] /
				(overrides.playcount
				and span['override'] / Raw(overrides.playcount)
				 or Raw(frame:expandTemplate {
					title = platform.name .. 'Count',
					args = { id = data.id },
				}))
		},
		mw.title.getCurrentTitle()--[[@cast -?]]:inNamespace(0)
			and Raw(
				('[[Category:%s年投稿至%s的歌曲]]'):format(data.date.year, platform.name)
			 .. ('[[Category:%s月%s日投稿至%s的歌曲]]'):format(data.date.month, data.date.day, platform.name)
			) or nil,
	}

	if mw.title.getCurrentTitle()--[[@cast -?]]:inNamespace(0) then
		card['data-json'] = mw.text.jsonEncode({
			id = data.id,
			date = data.date,
			platform = { name = platform.name }
		})
	end

	return card;
end

local p = {
	components = c,
}

local function get_by_aliases(t, ...)
	local n = select('#', ...)
	for i = 1, n do
		local key = select(i, ...)
		local item = t[key]
		if item ~= nil then
			return item
		end
	end
	return nil
end

---@param date_str string?
---@return Date?
local function parse_date(date_str)
	if not date_str then return nil end
	local parts = strings.split(date_str, '%s*[-/年月日]%s*', true)
	local year = tonumber(parts[1])
	local month = tonumber(parts[2])
	local day = tonumber(parts[3])
	if year and month and day then
		return { year = year, month = month, day = day }
	else
		return nil
	end
end

--[[
image
圖片信息 + 图片信息
圖片大小 > 图片大小
tabs

歌曲名称
投稿时间 + 其他资料
演唱
UP主 > P主
收录专辑 > 各平台id&date + 链接

颜色
颜色1
颜色2
颜色3
文字样式
文字样式1
文字样式2
文字样式3
]]
function p.from_args(uncleaned_args)
	local args = {}  ---@type table<string, string | nil>
	for k, v in pairs(uncleaned_args) do
		args[k] = clean(v)
	end

	function platform_from_arg(prefix)
		local id = args[prefix..'_id']
		local date = parse_date(args[prefix..'_date']
			or get_by_aliases(args, '投稿时间', '投稿時間'))
		if not id or not date then return nil end -- TODO: 报错
		return {
			id = id,
			date = date,
		}
	end

	local props = {
		image = args.image and {
			file = args.image,
			caption = merge(args['圖片信息'], args['图片信息']),
			size = get_by_aliases(args, '圖片大小', '图片大小'),
		},
		tabs = args.tabs,
		title = args['歌曲名称'],
		uploader = args['UP主'],
		producer = args['P主'],
		note = args['其他资料'],
		singer = args['演唱'],
		albums = args['收录专辑'],
		platforms = {
			nico  = platform_from_arg('nnd'),
			bili  = platform_from_arg('bb'),
			acfun = platform_from_arg('ac'),
			ncm   = platform_from_arg('wyy'),
			ytb   = platform_from_arg('yt'),
		},
		submissions = args['投稿'],
		colors = {
			general = args['颜色'],
			args['颜色1'],
			args['颜色2'],
			args['颜色3'],
		},
		text_styles = {
			general = args['文字样式'],
			args['文字样式1'],
			args['文字样式2'],
			args['文字样式3'],
		}
	}
	return c.SongBox(props)
end

function p.from_frame(frame)
	return p.from_args(frame.args)
end

function p.from_parent(frame)
	local parent = frame:getParent()
	return p.from_frame(parent)
end

function p.card_from_args(uncleaned_args)
	local args = {}  ---@type table<string, string | nil>
	for k, v in pairs(uncleaned_args) do
		args[k] = clean(v)
	end
	
	local conversion = {
		nnd = "nico",
		bb  = "bili",
		yt  = "ytb",
		wyy = "ncm",
		ac  = "acfun"
	}
	local key = conversion[args[1]] or args[1]
	local data = {
		id = args[2],
		date = parse_date(args[3])
	}
	local overrides = {
		title = args[4],
		playcount = get_by_aliases(args, '再生', 'count'),
		class = args['class']
	}
	
	for platform in platform_data:filter(function (x) return x.key == key end) do
		return c.Card(platform, data, overrides)
	end
end

function p.card_from_frame(frame)
	return p.card_from_args(frame.args)
end

function p.card_from_parent(frame)
	local parent = frame:getParent()
	return p.card_from_frame(parent)
end

return p