You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							279 lines
						
					
					
						
							8.0 KiB
						
					
					
				
			
		
		
	
	
							279 lines
						
					
					
						
							8.0 KiB
						
					
					
				| local lvgl = require("lvgl")
 | |
| local widgets = require("widgets")
 | |
| local backstack = require("backstack")
 | |
| local font = require("font")
 | |
| local playback = require("playback")
 | |
| local queue = require("queue")
 | |
| local screen = require("screen")
 | |
| local theme = require("theme")
 | |
| 
 | |
| local img = {
 | |
|   play = lvgl.ImgData("//lua/img/play.png"),
 | |
|   pause = lvgl.ImgData("//lua/img/pause.png"),
 | |
|   next = lvgl.ImgData("//lua/img/next.png"),
 | |
|   prev = lvgl.ImgData("//lua/img/prev.png"),
 | |
|   shuffle = lvgl.ImgData("//lua/img/shuffle.png"),
 | |
|   repeat_src = lvgl.ImgData("//lua/img/repeat.png"), -- repeat is a reserved word
 | |
| }
 | |
| 
 | |
| local format_time = function(time)
 | |
|   time = math.floor(time)
 | |
|   return string.format("%d:%02d", time // 60, time % 60)
 | |
| end
 | |
| 
 | |
| local is_now_playing_shown = false
 | |
| 
 | |
| local icon_enabled_class = "icon_enabled"
 | |
| local icon_disabled_class = "icon_disabled"
 | |
| 
 | |
| return screen:new {
 | |
|   createUi = function(self)
 | |
|     self.root = lvgl.Object(nil, {
 | |
|       flex = {
 | |
|         flex_direction = "column",
 | |
|         flex_wrap = "wrap",
 | |
|         justify_content = "center",
 | |
|         align_items = "center",
 | |
|         align_content = "center",
 | |
|       },
 | |
|       w = lvgl.HOR_RES(),
 | |
|       h = lvgl.VER_RES(),
 | |
|     })
 | |
|     self.root:center()
 | |
| 
 | |
|     self.status_bar = widgets.StatusBar(self, {
 | |
|       back_cb = backstack.pop,
 | |
|       transparent_bg = true,
 | |
|     })
 | |
| 
 | |
|     local info = self.root:Object {
 | |
|       flex = {
 | |
|         flex_direction = "column",
 | |
|         flex_wrap = "wrap",
 | |
|         justify_content = "center",
 | |
|         align_items = "center",
 | |
|         align_content = "center",
 | |
|       },
 | |
|       w = lvgl.PCT(100),
 | |
|       h = lvgl.SIZE_CONTENT,
 | |
|       flex_grow = 1,
 | |
|     }
 | |
| 
 | |
|     local artist = info:Label {
 | |
|       w = lvgl.PCT(100),
 | |
|       h = lvgl.SIZE_CONTENT,
 | |
|       text = "",
 | |
|       text_font = font.fusion_10,
 | |
|       text_align = 2,
 | |
|     }
 | |
| 
 | |
|     local title = info:Label {
 | |
|       w = lvgl.PCT(100),
 | |
|       h = lvgl.SIZE_CONTENT,
 | |
|       text = "",
 | |
|       text_align = 2,
 | |
|     }
 | |
| 
 | |
|     local playlist = self.root:Object {
 | |
|       flex = {
 | |
|         flex_direction = "row",
 | |
|         justify_content = "center",
 | |
|         align_items = "center",
 | |
|         align_content = "center",
 | |
|       },
 | |
|       w = lvgl.PCT(100),
 | |
|       h = lvgl.SIZE_CONTENT,
 | |
|     }
 | |
| 
 | |
|     playlist:Object({ w = 3, h = 1 }) -- spacer
 | |
| 
 | |
|     local cur_time = playlist:Label {
 | |
|       w = lvgl.SIZE_CONTENT,
 | |
|       h = lvgl.SIZE_CONTENT,
 | |
|       text = "",
 | |
|       text_font = font.fusion_10,
 | |
|     }
 | |
| 
 | |
|     playlist:Object({ flex_grow = 1, h = 1 }) -- spacer
 | |
| 
 | |
|     local playlist_pos = playlist:Label {
 | |
|       text = "",
 | |
|       text_font = font.fusion_10,
 | |
|     }
 | |
|     playlist:Label {
 | |
|       text = "/",
 | |
|       text_font = font.fusion_10,
 | |
|     }
 | |
|     local playlist_total = playlist:Label {
 | |
|       text = "",
 | |
|       text_font = font.fusion_10,
 | |
|     }
 | |
| 
 | |
|     playlist:Object({ flex_grow = 1, h = 1 }) -- spacer
 | |
| 
 | |
|     local end_time = playlist:Label {
 | |
|       w = lvgl.SIZE_CONTENT,
 | |
|       h = lvgl.SIZE_CONTENT,
 | |
|       align = lvgl.ALIGN.RIGHT_MID,
 | |
|       text = format_time(0),
 | |
|       text_font = font.fusion_10,
 | |
|     }
 | |
|     playlist:Object({ w = 3, h = 1 }) -- spacer
 | |
| 
 | |
|     local scrubber = self.root:Slider {
 | |
|       w = lvgl.PCT(100),
 | |
|       h = 5,
 | |
|       range = { min = 0, max = 100 },
 | |
|       value = 0,
 | |
|     }
 | |
|     local scrubber_desc = widgets.Description(scrubber, "Scrubber")
 | |
| 
 | |
|     scrubber:onevent(lvgl.EVENT.RELEASED, function()
 | |
|       local track = playback.track:get()
 | |
|       if not track then return end
 | |
|       if not track.duration then return end
 | |
|       playback.position:set(scrubber:value() / 100 * track.duration)
 | |
|     end)
 | |
|     scrubber:onevent(lvgl.EVENT.VALUE_CHANGED, function()
 | |
|       if scrubber:is_dragged() then
 | |
|         local track = playback.track:get()
 | |
|         if not track then return end
 | |
|         if not track.duration then return end
 | |
|         cur_time:set {
 | |
|           text = format_time(scrubber:value() / 100 * track.duration)
 | |
|         }
 | |
|       end
 | |
|     end)
 | |
| 
 | |
|     local controls = self.root:Object {
 | |
|       flex = {
 | |
|         flex_direction = "row",
 | |
|         justify_content = "center",
 | |
|         align_items = "center",
 | |
|         align_content = "center",
 | |
|       },
 | |
|       w = lvgl.PCT(100),
 | |
|       h = lvgl.SIZE_CONTENT,
 | |
|       pad_column = 8,
 | |
|       pad_all = 2,
 | |
|     }
 | |
| 
 | |
|     controls:Object({ flex_grow = 1, h = 1 }) -- spacer
 | |
| 
 | |
|     local repeat_btn = controls:Button {}
 | |
|     repeat_btn:onClicked(function()
 | |
|       queue.repeat_track:set(not queue.repeat_track:get())
 | |
|     end)
 | |
|     local repeat_img = repeat_btn:Image { src = img.repeat_src }
 | |
|     theme.set_style(repeat_img, icon_enabled_class)
 | |
|     local repeat_desc = widgets.Description(repeat_btn)
 | |
| 
 | |
| 
 | |
|     local prev_btn = controls:Button {}
 | |
|     prev_btn:onClicked(function()
 | |
|       if playback.position:get() > 3 then
 | |
|         playback.position:set(0)
 | |
|       else
 | |
|         queue.previous()
 | |
|       end
 | |
|     end)
 | |
|     local prev_img = prev_btn:Image { src = img.prev }
 | |
|     theme.set_style(prev_img, icon_enabled_class)
 | |
|     local prev_desc = widgets.Description(prev_btn, "Previous track")
 | |
| 
 | |
|     local play_pause_btn = controls:Button {}
 | |
|     play_pause_btn:onClicked(function()
 | |
|       playback.playing:set(not playback.playing:get())
 | |
|     end)
 | |
|     play_pause_btn:focus()
 | |
|     local play_pause_img = play_pause_btn:Image { src = img.pause }
 | |
|     theme.set_style(play_pause_img, icon_enabled_class)
 | |
|     local play_pause_desc = widgets.Description(play_pause_btn, "Play")
 | |
| 
 | |
|     local next_btn = controls:Button {}
 | |
|     next_btn:onClicked(queue.next)
 | |
|     local next_img = next_btn:Image { src = img.next }
 | |
|     theme.set_style(next_img, icon_disabled_class)
 | |
|     local next_desc = widgets.Description(next_btn, "Next track")
 | |
| 
 | |
|     local shuffle_btn = controls:Button {}
 | |
|     shuffle_btn:onClicked(function()
 | |
|       queue.random:set(not queue.random:get())
 | |
|     end)
 | |
|     local shuffle_img = shuffle_btn:Image { src = img.shuffle }
 | |
|     theme.set_style(shuffle_img, icon_enabled_class)
 | |
|     local shuffle_desc = widgets.Description(shuffle_btn)
 | |
| 
 | |
|     controls:Object({ flex_grow = 1, h = 1 }) -- spacer
 | |
| 
 | |
| 
 | |
|     self.bindings = self.bindings + {
 | |
|       playback.playing:bind(function(playing)
 | |
|         if playing then
 | |
|           play_pause_img:set_src(img.pause)
 | |
|           play_pause_desc:set { text = "Pause" }
 | |
|         else
 | |
|           play_pause_img:set_src(img.play)
 | |
|           play_pause_desc:set { text = "Play" }
 | |
|         end
 | |
|       end),
 | |
|       playback.position:bind(function(pos)
 | |
|         if not pos then return end
 | |
|         if not scrubber:is_dragged() then
 | |
|           cur_time:set {
 | |
|             text = format_time(pos)
 | |
|           }
 | |
|           local track = playback.track:get()
 | |
|           if not track then return end
 | |
|           if not track.duration then return end
 | |
|           scrubber:set { value = pos / track.duration * 100 }
 | |
|         end
 | |
|       end),
 | |
|       playback.track:bind(function(track)
 | |
|         if not track then return end
 | |
|         if track.duration then
 | |
|           end_time:set { text = format_time(track.duration) }
 | |
|         else
 | |
|           end_time:set { text = format_time(playback.position:get()) }
 | |
|         end
 | |
|         title:set { text = track.title }
 | |
|         artist:set { text = track.artist }
 | |
|       end),
 | |
|       queue.position:bind(function(pos)
 | |
|         if not pos then return end
 | |
|         playlist_pos:set { text = tostring(pos) }
 | |
| 
 | |
|         theme.set_style(
 | |
|           next_img, pos < queue.size:get() and icon_enabled_class or icon_disabled_class
 | |
|         )
 | |
|       end),
 | |
|       queue.random:bind(function(shuffling)
 | |
|         theme.set_style(shuffle_img, shuffling and icon_enabled_class or icon_disabled_class)
 | |
|         if shuffling then
 | |
|           shuffle_desc:set { text = "Disable shuffle" }
 | |
|         else
 | |
|           shuffle_desc:set { text = "Enable shuffle" }
 | |
|         end
 | |
|       end),
 | |
|       queue.repeat_track:bind(function(en)
 | |
|         theme.set_style(repeat_img, en and icon_enabled_class or icon_disabled_class)
 | |
|         if en then
 | |
|           repeat_desc:set { text = "Disable track repeat" }
 | |
|         else
 | |
|           repeat_desc:set { text = "Enable track repeat" }
 | |
|         end
 | |
|       end),
 | |
|       queue.size:bind(function(num)
 | |
|         if not num then return end
 | |
|         playlist_total:set { text = tostring(num) }
 | |
|       end),
 | |
|     }
 | |
|   end,
 | |
|   onShown = function() is_now_playing_shown = true end,
 | |
|   onHidden = function() is_now_playing_shown = false end,
 | |
|   pushIfNotShown = function(self)
 | |
|     if not is_now_playing_shown then
 | |
|       backstack.push(self:new())
 | |
|     end
 | |
|   end
 | |
| }
 | |
| 
 |