今天是我的十八岁生日。
最近我一直在折腾我 vibecoding 的 xlgui 框架,我想,要不就自己用自己的xlgui写个demo试试,有一个词形容这种行为,叫dogfooding,这两天我一直在写这个,一开始我只想单曲播放,后面把列表和专辑封面,lrc支持加了上去。久违的手写代码,而不是ai agent。
我下了一堆几十mb的flac用着我写的这个听歌。有意思的是,不少flac在windows 11自带的媒体播放器里打不开,反而在我的player下正常。

我懒得说了,直接把代码贴着。
#include <xlgui/xlgui.hpp>
#include <fmt/format.h>
#include <SFML/Audio.hpp>
#include <taglib/fileref.h>
#include <taglib/tag.h>
#include <random>
#include <regex>
using namespace xlgui::dsl;
namespace xl=xlgui;
namespace fs=std::filesystem;
std::vector<std::string> sfmlAudioSupportedTypes = {
".mp3", ".ogg", ".flac", ".wav"
};
class AppVM
{ public:
inline static Ref<sf::Time> totalPlayTime;
inline static Ref<sf::Time> currentPlayTime;
inline static Ref<std::string> songName = "未播放歌曲";
inline static Ref<std::string> artistName = "拖拽音乐文件至此以播放";
inline static Ref<std::string> albumName = "";
inline static Ref<bool> isPlaying = false;
inline static Ref<float> volume = 1.0f;
inline static Ref<std::vector<fs::path>>
playList = {};
inline static Ref<std::vector<unsigned char>> coverData;
inline static Ref<bool> isSongHaveCover = false;
inline static Ref<std::string> currentPlayFilePath = "";
inline static Ref<int> currentPlayListIdx = 0;
inline static std::vector<std::pair<std::chrono::microseconds, std::string>> currentPlayLyrics;
inline static Ref<int> currentPlayLyricsIdx = 0;
inline static Ref<std::string> currentPlayLyricLine = "";
enum class PlayListRule {
Loop = 0,
LoopSong,
Count,
};
inline static Ref<PlayListRule> playListRule = PlayListRule::Loop;
inline static Ref<bool> isRandom = false;
inline static sf::Music music;
inline static std::mt19937& GetRandomDevice() {
thread_local std::mt19937 rd(std::random_device{}());
return rd;
}
static void ResetCurrentPlayTime()
{
currentPlayTime = sf::Time::Zero;
}
static void Play(const std::filesystem::path& path, bool scan = true)
{
if (!music.openFromFile(path))
{
XL_WARN("play failed, {}", path.string());
currentPlayFilePath = "";
songName = "无法播放的文件类型";
artistName = "";
totalPlayTime = sf::Time::Zero;
currentPlayTime = sf::Time::Zero;
isPlaying = false;
currentPlayLyricLine = "";
} else {
ResetCurrentPlayTime();
currentPlayFilePath = path.string();
if (scan) {
LoadToPlayList(path);
}
music.play();
UpdateSongInfo(path);
isPlaying = true;
}
}
static void Play()
{
if (currentPlayFilePath.Get() != "") {
music.play();
isPlaying = true;
}
}
static void Play(int idx)
{
if (playList.Get().size()==0) {return;}
if (idx >= playList.Get().size()) { idx=0; }
else if (idx < 0) { idx=playList.Get().size()-1; }
Play(playList.Get()[idx], false);
currentPlayListIdx = idx;
}
static void Pause()
{
if (isPlaying)
{
music.pause();
isPlaying = false;
}
}
static void TogglePlay()
{
if (isPlaying)
{
Pause();
} else {
Play();
}
}
// lrc
static void LoadLyrics(const std::filesystem::path& path)
{
currentPlayLyricsIdx = 0;
currentPlayLyrics.clear();
currentPlayLyricLine = "";
std::ifstream file(path);
if (!file.is_open()) {
return;
}
std::string buffer;
while (std::getline(file, buffer))
{
static const std::regex pattern(R"(\[(\d{2}):(\d{2})\.(\d{2,3})\](.*))");
std::smatch sm;
if (std::regex_match(buffer, sm, pattern))
{
auto ms_str = sm[3].str();
auto ms = stoi(sm[3]);
if (ms_str.length() == 2) {
ms *= 10;
}
currentPlayLyrics.push_back({
std::chrono::minutes(std::stoi(sm[1])) +
std::chrono::seconds(std::stoi(sm[2])) +
std::chrono::milliseconds(ms),
sm[4]
});
}
}
}
static void UpdateSongInfo(const std::filesystem::path& path)
{
TagLib::FileRef file(path.c_str(), false);
if (!file.isNull() && file.tag() && file.file())
{
songName = file.tag()->title().toCString(true);
artistName = file.tag()->artist().toCString(true);
TagLib::List<TagLib::VariantMap> pictures = file.file()->complexProperties("PICTURE");
if (pictures.isEmpty())
{
isSongHaveCover = false;
(*coverData).clear();
} else {
// 不耗时,直接同步
const auto& picMap = pictures.front();
TagLib::ByteVector imageData = picMap["data"].toByteVector();
const auto* dataPtr = reinterpret_cast<const unsigned char*>(imageData.data());
coverData = std::vector<unsigned char>(dataPtr, dataPtr + imageData.size());
isSongHaveCover = true;
}
} else {
isSongHaveCover = false;
(*coverData).clear();
songName = reinterpret_cast<const char*>(path.stem().u8string().c_str());
(*artistName).clear();
}
// 如果没获取到直接文件名赋给songName(无后缀)
if (songName.Get() == "") {
// c++ 20 shabi
songName = reinterpret_cast<const char*>(path.stem().u8string().c_str());
}
auto lrcpath = path;
lrcpath.replace_extension(".lrc");
LoadLyrics(lrcpath);
// fallback
if (currentPlayLyrics.empty())
{
lrcpath = path.parent_path() / "lyrics" / path.filename();
lrcpath.replace_extension(".lrc");
LoadLyrics(lrcpath);
}
if (currentPlayLyrics.empty())
{
lrcpath = path.parent_path() / "lrc" / path.filename();
lrcpath.replace_extension(".lrc");
LoadLyrics(lrcpath);
}
totalPlayTime = music.getDuration();
}
static void StepBack(float val)
{
if (music.getPlayingOffset() - val * music.getDuration() >= sf::Time::Zero)
music.setPlayingOffset(music.getPlayingOffset() - val * music.getDuration());
SyncLyrics();
}
static void StepForward(float val)
{
music.setPlayingOffset(music.getPlayingOffset() + val * music.getDuration());
SyncLyrics();
}
static void ChangeCurrentPlayTime(float val)
{
auto offset = val * music.getDuration();
currentPlayTime = offset;
music.setPlayingOffset(currentPlayTime);
SyncLyrics();
}
static void SetVolume(float val)
{
if (val < 0.0f) { val = 0.0f; }
else if (val > 1.0f) { val = 1.0f; }
volume = val;
music.setVolume(volume * 100.0f);
}
static void SetPlayListRule(PlayListRule rule) { playListRule = rule; }
static void SetRandom(bool random) { isRandom = random; }
static void LoadToPlayList(const std::filesystem::path& path)
{
fs::path p;
if (!fs::is_directory(path))
{
p = path.parent_path();
} else {
p = path;
}
std::vector<fs::path> playlist;
auto it = fs::directory_iterator(p);
int idx = 0, curridx = 0;
for (const auto& e : it)
{
if (e.is_regular_file())
{
for (auto& t : sfmlAudioSupportedTypes)
{
auto ext = e.path().extension().string();
for (char& c : ext)
{
c = std::tolower(static_cast<unsigned char>(c));
}
if (ext == t)
{
playlist.push_back(e.path());
if (path == e.path()) { curridx = idx; }
idx++;
}
}
}
}
playList = playlist;
currentPlayListIdx = curridx;
}
static void NextSong(bool is_force = false)
{
if (playListRule == PlayListRule::LoopSong) {
if (!is_force) {
Play();
return;
}
}
else
{
if (isRandom) {
std::uniform_int_distribution<int> dis(0, playList.Get().size()-1);
Play(dis(GetRandomDevice()));
return;
}
}
Play(currentPlayListIdx+1);
}
static void PrevSong(bool is_force = false)
{
if (isRandom) {
std::uniform_int_distribution<int> dis(0, playList.Get().size()-1);
Play(dis(GetRandomDevice()));
return;
}
Play(currentPlayListIdx-1);
}
inline static void Sync()
{
if (isPlaying) {
currentPlayTime = music.getPlayingOffset();
if (music.getStatus() == sf::Music::Status::Stopped)
{
NextSong();
}
SyncLyrics();
}
}
inline static void SyncLyrics()
{
if (currentPlayLyrics.empty()) {
return;
}
auto currms = std::chrono::microseconds(currentPlayTime.Get().asMicroseconds());
// 找到第一个大于当前时间的歌词
auto it = std::upper_bound(
currentPlayLyrics.begin(),
currentPlayLyrics.end(),
currms,
[](const std::chrono::microseconds& time, const auto& item) {
return time < item.first;
}
);
if (it == currentPlayLyrics.begin()) {
// 歌曲前奏阶段显示artistName
currentPlayLyricsIdx = 0;
currentPlayLyricLine = "";
} else {
// 找到的歌词是it的前一个位置
int idx = static_cast<int>(std::distance(currentPlayLyrics.begin(), it) - 1);
currentPlayLyricsIdx = idx;
currentPlayLyricLine = currentPlayLyrics[idx].second;
}
}
};
std::string timeToStr(const sf::Time& time)
{
return fmt::format("{:02d}:{:02d}", static_cast<int>(time.asSeconds() / 60)
, static_cast<int>((int)time.asSeconds() % 60));
}
void applyAppStyles(xl::Application& app)
{
static bool initStyle = [&app](){
WidgetRef(app.getRoot())
.select(".player-area button")
.padding(4, 4).radius(50_pct).bg(Color::Transparent)
.fontSize(20)
.end();
return true;
}();
if (xl::IsSystemLightMode()) {
WidgetRef(app.getRoot())
.select(".player-area button").fg(Color::Black)
.select(".artist-text").fg("#3a3939")
;
} else {
WidgetRef(app.getRoot())
.select(".player-area button").fg(Color::White)
.select(".artist-text").fg("#dbdbdb")
;
}
}
int main(int argc, char** argv)
{
xl::Application app("xl Music Player");
app.Backdrop(WindowBackdropType::Acrylic, IsSystemDarkMode());
app.backdropTint(Color::Transparent);
app.AddChildren({
Spacer(),
Flex({
VStack({
HStack({
ImageBox().id("cover-image").radius(16).width(320).height(320)
.shadow(
0, 2, 4, Color("#0000008f")
)
.src(AppVM::coverData)
}).hcenter().visible(AppVM::isSongHaveCover).marginBottom(16)
,
VStack({
Text(AppVM::songName).fontSize(26),
Text([](){
if (*AppVM::currentPlayLyricLine != "") {
if (*AppVM::artistName == "") {
return *AppVM::currentPlayLyricLine;
}
return fmt::format("{}: {}",
*AppVM::artistName,
*AppVM::currentPlayLyricLine);
}
return AppVM::artistName.Get();
}).fontSize(18).className("artist-text"),
// Text(AppVM::currentPlayLyricLine).fontSize(16)
}),
VStack({
HStack({
Text([](){ return timeToStr(AppVM::currentPlayTime); }),
Slider([](){
return AppVM::currentPlayTime.Get().asSeconds()
/ AppVM::totalPlayTime.Get().asSeconds();
}).width(40_vw).minWidth(320_dp)
.onValueChanged([](float val){
AppVM::ChangeCurrentPlayTime(val);
}),
Text([](){ return timeToStr(AppVM::totalPlayTime); }),
}),
HStack({
Button("🔀").bg([](){ return AppVM::isRandom ? Color("#2563eb") : Color::Transparent;})
.onclick([](){ AppVM::isRandom = !AppVM::isRandom; }),
Button([](){
if (AppVM::playListRule == AppVM::PlayListRule::Loop)
{ return "🔁"; }
return "🔂";
}).onclick([](){
int current = static_cast<int>(AppVM::playListRule.Get());
int total = static_cast<int>(AppVM::PlayListRule::Count);
AppVM::playListRule = static_cast<AppVM::PlayListRule>((current + 1) % total);
}),
Button("⏮").onclick([](){
AppVM::PrevSong(true);
}),
Button([](){
return AppVM::isPlaying ?
"⏸" : "▶";
}).onclick([](){
if (AppVM::isPlaying)
{ AppVM::Pause(); } else { AppVM::Play(); }
}),
Button("⏭").onclick([](){
AppVM::NextSong(true);
}),
HStack({
Button([](){
if (AppVM::volume <= 0) return "🔇";
else if (AppVM:: volume <= 0.3f) return "🔈";
else if (AppVM:: volume <= 0.6f) return "🔉";
else if (AppVM:: volume <= 1.0f) return "🔊";
return "🔊";
}).onclick([]()
{ static float before = 0.5f;
if (AppVM::volume <= 0) {
AppVM::SetVolume(before > 0.0f ? before : 0.5f);
} else {
before = AppVM::volume.Get();
AppVM::SetVolume(0.0f);
} }),
Flex({
Slider(AppVM::volume).onValueChanged([](float val){
AppVM::SetVolume(val);
}).width(100_pct).marginRight(8)
}).bg("#c2c2c265").radius(12)
}),
Button("◨").onclick([&app](){
static bool isOpen = false;
isOpen = !isOpen;
WidgetRef(app.GetChild("#playlist-view")).visible(isOpen);
WidgetRef(app.GetChild("#main")).justifyContent(isOpen ? JustifyContent::FlexEnd : JustifyContent::Center);
}),
}).hcenter().className("player-area"),
}).hcenter(),
}).center().textAlign(TextAlign::Center),
HStack({
List([](){
std::vector<std::string> r;
r.reserve(AppVM::playList.Get().size());
for (auto& i : AppVM::playList.Get())
{
r.push_back(reinterpret_cast<const char*>(i.stem().u8string().c_str()));
}
return r;
})
.radius(12).bg("#c2c2c23b")
.selected(AppVM::currentPlayListIdx)
.onselect([](int idx){
AppVM::Play(idx);
}).width(30_vw).height(100_vh)
}).visible(false).id("playlist-view").paddingLeft(10_pct)
})
.width(100_vw).height(100_vh).alignItems(Alignment::Center).justifyContent(JustifyContent::Center).id("main"),
Spacer(),
});
applyAppStyles(app);
app.OnSystemThemeChanged([&app](){
applyAppStyles(app);
});
app.OnDropFile([](const std::string& path) {
AppVM::Play(std::filesystem::u8path(path));
});
app.SetInterval([]() mutable {
AppVM::Sync();
}, 1000);
app.onKeyDown([&app](xl::Key k){
if (k == xl::Key::Space)
{
AppVM::TogglePlay();
}
if (k == xl::Key::Left || k == xl::Key::A)
{
AppVM::StepBack(0.01);
AppVM::Sync();
}
if (k == xl::Key::Right || k == xl::Key::D)
{
AppVM::StepForward(0.01);
AppVM::Sync();
}
if (k == xl::Key::Up)
{
AppVM::SetVolume(AppVM::volume+0.05f);
}
if (k == xl::Key::Down)
{
AppVM::SetVolume(AppVM::volume-0.05f);
}
});
// 大鸡巴猛火爆操微软
#ifndef _WIN32
if (argc > 1) {
AppVM::Play(std::filesystem::u8path(argv[1]));
}
#endif
app.Run();
}
$$\downarrow$$



我懒得写什么了,我想睡觉。
我还想了下 xlgui 的哲学:
$$ \text{State} \leftrightarrow \text{Interface} \leftrightarrow \text{Actor} $$ $$ \quad\downarrow\quad $$ $$ \text{State} \leftrightarrow \text{UI} \leftrightarrow \text{Human} $$ $$ \quad\downarrow\quad $$ $$ \text{State} \leftrightarrow \text{xlgui} \leftrightarrow \text{Human} $$
在我看来,这个世界可以被抽象为state,一个随行动不断变化的state,比如说你拿起一个桌上的水杯,你的感官(向你)、四肢(向外)充当了interface,你发出神经传导,让你的手去拿水杯,水杯的位置被改变(state changed),同理你的感官也捕捉到了水杯位置的变化。
同理,ui也是一种interface,它的作用就是负责人与state的交互,作为一种桥梁,这有点像操作系统的比喻,说自己是硬件和软件的桥梁。在我看来,好的state<->ui(interface)设计,即是就算ui变成另一个interface,无需改变state逻辑,也能够与interface相连接,interface作为桥梁与人或其它actor相连。
我懒得写了。最后祝我这未来十年里,睡一万个18-20岁的漂亮日本女生,买辆Supra,买辆保时捷,存款有个千万。
Copyright © 2026 后术. All rights reserved.