之前已经发了一篇博客简述了如何阅读这个项目,尽管这个项目已经开源很久了,但我找了很久都没有找到怎么从播放列表移除歌曲,那就自己动手实现,再提个 PR 吧。
运行起来应用后通过 Inspector(Ctrl+Shift+I
)找到希望放置按钮的位置:专辑按钮的旁边。
第一步就是修改UI文件,把这个按钮显示出来。照葫芦画瓢,先加一个按钮。
<child>
<object class="GtkButton" id="remove_button">
<property name="halign">end</property>
<property name="valign">center</property>
<property name="icon-name">app-remove-symbolic</property>
<property name="tooltip-text" translatable="yes">Remove song</property>
<signal name="clicked" handler="remove_button_clicked_cb" swapped="true" />
<style>
<class name="flat" />
</style>
</object>
</child>
每一个显示歌曲信息的行通常都有这样几个按钮:艺术家、收藏、专辑。然而在专辑的详细信息里面歌曲的信息就不需要专辑按钮了。行 Widget 是通用的,但是行中的一些 widget 是应该隐藏的。也就是说,移除歌曲按钮应该只在播放列表中才是可见的,移除按钮只显示在下面这个提交中实现:
https://github.com/wngtk/netease-cloud-music-gtk/commit/cb0a143f01c8d02a58b073677b94c14db0c18dfd
原来的代码中有大量的代码拷贝,我添加新的按钮的时候也尽可能的保持其他的地方不变,因此我需要在多处做拷贝粘贴再修改一下字符,导致了两处忘记修改而调试了两个小时。
而单独这一个 commit 中还没有很好的隐藏按钮,因为在代码拷贝的过程中有两处没有改名字,
f3d90c中修改好后这个按钮才显示在合理的地方。
第二步,实现移除正在播放歌曲。几乎所有的按钮的操作最后都是通过发送一个消息来通知 process_action(),因此我们也需要加入一个新的Action。
有了新 Action 还只是能响应按钮,真正的业务逻辑还没实现。在这里简述实现的流程:
- 如果移除的歌曲是正在播放的歌曲,并且移除后还有歌曲可以播放,那么播放下一曲
- 从播放列表中移除想要移除的歌曲
- 更新播放列表的展示
流程确实简单,但是在实现的过程中遇到的细节却困扰了我很久。一个主要的问题是数据的更新并不会引发视图的更新。我需要指定调用更新才能更新视图。因为播放列表中完全存了另外一份数据,同样的歌曲列表的视图,和同样歌曲列表,播放列表视图完全拷贝了一份,所以要实现更新且不破坏原来的代码我只能重新初始化页面。
在胡乱加代码以测试到底为什么之前的修改没有失效的时候,还错误的设置了一个不存在的属性,引入了一个 bug,不过在调试器的帮助下轻松定位到崩溃发生的地方,删除不该有的代码便修复了。
总的来说实现过程是曲折的。最后附上一些在改代码的时候的吐槽。
点我看吐槽
因为代码大量使用了异步,调试的时候还是会有一些不方便,没有办法线性的跟踪,只能在处理信号的地方打断点,但是不知道信号是从哪里发起的。我的溯源的方法是查看按钮的回调函数,但是当自己尝试调用原有的 player_controls 的方法,就找不到额外的信号是怎么被发起的,例如我调用 player_controls 里面的 play() 方法播放器会发起一个播放下一曲的信号,但是实际上 play() 方法里面没有做下一曲的事情,那就是其他的注册的信号处理函数导致了播放下一曲,但是一时半会儿找不到具体的问题所在。或许响应按键操作用异步没什么不对,但是现在的代码中整个播放器的业务逻辑也依赖于异步通道(async_channel)传递信息。
因此,在现有的代码中,单纯的给 plyer_controls 指定一个歌曲去 play 不能正确做到,因为其中可能引发其他的异步信号,导致代码不能按照预期执行。这算是有隐藏的控制流。
作者没有使用 MVC,播放器的功能和 UI 是强耦合的,切换下一曲严重依赖图形界面上的按钮的回调函数。要避免意外情况出现,所有的功能,最好通过模拟 UI 操作进行(尽管这样不好,但是原来的代码里面就是这样做的)。尽管所有的异步的信号最终都会汇聚到 process_action() 函数里面来处理,但是如果单纯的操作 Action 往往和期望不一样,因为 Action 执行之前,有一些副作用发生。不通过模拟 UI 操作来实现移除歌曲的功能,实现的效果往往不如人意。例如
很遗憾,原有的代码并没有做视图(View)和数据(Model)的绑定, 数据改变后,视图并不能自动更新。换句话说,在删除播放列表的歌曲的业务逻辑实现后,UI 并没有自动更新。原来的代码用的是 ListBox,
花时间最多的部分是界面的显示,标记正在播放的图标要显示在正确的行,删除了的歌曲应该不再展示,删除播放列表歌曲的过程中导致歌词不显示了。在已有的接口和设计上,播放列表(playlist_lyrics)可能没有考虑过要修改,播放列表就提供的方法有:初始化,更新歌词,切换播放行。
脑袋中想着既然这里没有 MVC, 我就要手动更新视图。因此我尝试直接修改表达视图的对象,可惜的是最终以失败告终,因为一直存在着小问题。或许那种方法的效果是可行的,但是并没有很好的将已有的代码利用起来。因为整个播放器的控制是由 player_controls 和 window 两个对象控制的,有的操作放到 player_controls 里面完成更方便,而为了利用现有的 window 对象已有的方法有的操作放到 window 会更方便。如果严格一点按照设计模式来做,PlyerControl 应该负责所有的数据的控制,不应该有些东西放到了 window 去做。
而移除的歌曲不再显示是通过手动做页面路由,将 Action::ToSongListPage 执行的部分逻辑再执行一次,而不退出当前的播放列表页面。为了避免破坏原有的代码,不修改 Action::ToSongListPage,然而即便是模拟了重新进入页面,依旧会出现没有正确标记正在播放的歌曲。我没有找到问题的根源,因为一切看起来都是正常的,也没有什么头绪去定位问题到底是什么。
最后没有正确标记的解决方法是由 player_controls 移除歌曲后发起一个Action::UpdatePlayListStatus(playlist.get_position()
,因为 player_controls 已经有代码使用了 playlist.get_position(),而 UpdatePlayListStatus 又需要一个 position,就只好将这一部分的代码放在 player_controls。相当于是最后又委派给全局来实现。这里的做法来自于下一曲按钮的回调函数会发送 UpdatePlayListStatus。
sender
.send_blocking(
Action::UpdatePlayListStatus(playlist.get_position())
).unwrap();