Skip to content

pure golang GUI mode #71

@ZenLiuCN

Description

@ZenLiuCN

Base on mordernc.org/tk9.0

main entry

package main

import (
	"context"
	_ "embed"
	"encoding/json"
	"github.com/ZenLiuCN/fn"
	"github.com/orestonce/m3u8d"
	"github.com/orestonce/m3u8d/m3u8dcpp"
	. "modernc.org/tk9.0"
	_ "modernc.org/tk9.0/themes/azure"
	"os"
	"runtime"
	"strconv"
	"strings"
	"sync"
	"time"
)

//go:embed favicon.ico
var ico []byte

type conf = func(col int) ([]Opt, int)

var rows = 0
var pad = Opts{Padx(3), Pady(3)}

func row(val ...any) {
	r := Row(rows)
	i := 0
	xi := 0
	var w Widget
	var s []Opt = append([]Opt{}, pad...)
	for _, v := range val {
		switch x := v.(type) {
		case Widget:
			if w != nil {
				Grid(w, append(s, r, Column(i))...)
				//println(n, i)
				i += xi + 1
			}
			w = x
			s = s[:0]
			s = append(s, pad...)
		case Opt:
			s = append(s, x)
		case conf:
			o, d := x(i)
			xi += d
			s = append(s, o...)
		}
	}
	Grid(w, append(s, r, Column(i))...)
	rows++
}

func colSpan(n int, opts ...Opt) conf {
	return func(i int) ([]Opt, int) { return append(opts, Columnspan(n)), n }
}

var req m3u8d.StartDownload_Req
var (
	uri, folder, file, skipScript, proxy, tempFolder                                       *TEntryWidget
	threadVal                                                                              *LabelWidget
	threads, Insecure, SkipRemoveTs, UseServerSideTime, SkipMergeTs, WithSkipLog, DebugLog *VariableOpt
	merge, download                                                                        *TButtonWidget
	ctx                                                                                    context.Context
	ctxCC                                                                                  context.CancelFunc
)

func syncGUI() {
	if req.Insecure {
		Insecure.Set(1)
	}
	if req.SkipRemoveTs {
		SkipRemoveTs.Set(1)
	}
	if req.UseServerSideTime {
		UseServerSideTime.Set(1)
	}
	if req.SkipMergeTs {
		SkipMergeTs.Set(1)
	}
	if req.ThreadCount == 0 {
		req.ThreadCount = runtime.NumCPU()
	}
	if req.WithSkipLog {
		WithSkipLog.Set(1)
	}
	if req.DebugLog {
		DebugLog.Set(1)
	}
	uri.Configure(Textvariable(req.M3u8Url))
	folder.Configure(Textvariable(req.SaveDir))
	skipScript.Configure(Textvariable(req.SkipTsExpr))
	tempFolder.Configure(Textvariable(req.TsTempDir))
	proxy.Configure(Textvariable(req.SetProxy))
	threads.Set(req.ThreadCount)
}
func syncRequest(mute bool) bool {
	req.M3u8Url = uri.Textvariable()
	req.SetProxy = proxy.Textvariable()
	req.SaveDir = folder.Textvariable()
	req.FileName = file.Textvariable()
	req.SkipTsExpr = skipScript.Textvariable()
	req.TsTempDir = tempFolder.Textvariable()
	//req.ProgressBarShow = false
	var err error
	req.ThreadCount, err = strconv.Atoi(truncate(threads.Get()))
	if err != nil || req.ThreadCount < 1 {
		if !mute {
			MessageBox(Default("ok"), Title("错误"), Detail("线程数只能为正整数:"+threads.Get()), Parent(App), Type("ok"))
		}
		return false
	}
	if !mute {
		if req.M3u8Url == "" {
			MessageBox(Default("ok"), Title("错误"), Detail("无效的M3U8地址:"), Parent(App), Type("ok"))
			return false
		}
	}
	saveConfig()
	return true
}
func loadConfig() {
	f, err := os.Open("config.json")
	if err == nil {
		fn.Panic(json.NewDecoder(f).Decode(&req))
		_ = f.Close()
	}
}
func saveConfig() {
	f, _ := os.OpenFile("config.json", os.O_CREATE|os.O_TRUNC, os.ModePerm)
	_ = json.NewEncoder(f).Encode(&req)
	_ = f.Close()
}
func main() {
	loadConfig()
	var render []func()
	renderLock := &sync.RWMutex{}
	uri = TEntry(Textvariable(req.M3u8Url))
	row(
		Label(Txt("m3u8地址:")), Sticky("nw"),
		uri, colSpan(9, Sticky("new")),
		TButton(Txt("curl模式"), Command(func() {
			syncRequest(true)
			if req.M3u8Url == "" {
				MessageBox(Default("ok"), Title("错误"), Detail("无效的M3U8地址:"), Parent(App), Type("ok"))
				return
			}
			var d = CurlData{
				Text: m3u8d.RunDownload_Req_ToCurlStr(req),
			}
			curlDialog(&d).ShowModal()
			if d.Ok {
				v := m3u8d.ParseCurlStr(d.Text)
				if v.ErrMsg != "" {
					MessageBox(Default("ok"), Title("错误"), Detail("无效的Curl:"+v.ErrMsg), Parent(App), Type("ok"))
				} else {
					req = v.DownloadReq
				}
			}
			syncGUI()
		})), Padx(10),
	)
	folder = TEntry(Textvariable(req.SaveDir))
	row(
		Label(Txt("保存位置:")), Sticky("nw"),
		folder, colSpan(9, Sticky("new")),
		TButton(Txt("选择"), Command(func() {
			dir := ChooseDirectory(Initialdir(folder.Variable()), Mustexist(true), Parent(App), Title("保存位置"))
			if dir != "" {
				folder.Configure(Textvariable(dir))
			}
		})),
	)
	file = TEntry(Textvariable(req.FileName))
	row(
		Label(Txt("保存文件名:")), Sticky("nw"),
		file, colSpan(9, Sticky("new")),
	)
	skipScript = TEntry(Textvariable(req.SkipTsExpr))
	row(
		Label(Txt("跳过TS信息:")), Sticky("nw"),
		skipScript, colSpan(9, Sticky("new")),
	)
	proxy = TEntry(Textvariable(req.SetProxy))
	row(
		Label(Txt("代理设置:")), Sticky("nw"),
		proxy, colSpan(9, Sticky("new")),
	)
	n := runtime.NumCPU()
	if req.ThreadCount <= 0 {
		req.ThreadCount = n
	}
	threads = Variable(req.ThreadCount)
	threadVal = Label(Textvariable(strconv.Itoa(req.ThreadCount)))
	row(
		Label(Txt("线程数:")), Sticky("nw"),
		threadVal, Sticky("nw"),
		TScale(From(1), To(4*n), threads, Command(func() {
			threadVal.Configure(Textvariable(truncate(threads.Get())))
		})), colSpan(8, Sticky("new")),
	)
	tempFolder = TEntry(Textvariable(req.TsTempDir))
	row(
		Label(Txt("临时文件夹:")), Sticky("nw"),
		tempFolder, colSpan(9, Sticky("new")),
		TButton(Txt("选择"), Command(func() {
			dir := ChooseDirectory(Initialdir(tempFolder.Variable()), Mustexist(true), Parent(App), Title("下载临时保存位置"))
			if dir != "" {
				tempFolder.Configure(Textvariable(dir))
			}
		})),
	)
	Insecure = Variable(0)
	SkipRemoveTs = Variable(0)
	row(
		TCheckbutton(Txt("允许不安全的https请求"), Insecure, Onvalue(1), Offvalue(-1), Command(func() {
			req.Insecure = Insecure.Get() == "1"
		})), colSpan(2, Sticky("nw")),
		TCheckbutton(Txt("不删除下载的ts文件"), SkipRemoveTs, Onvalue(1), Offvalue(-1), Command(func() {
			req.SkipRemoveTs = SkipRemoveTs.Get() == "1"
		})), colSpan(2, Sticky("nw")),
	)
	UseServerSideTime = Variable(0)
	SkipMergeTs = Variable(0)
	row(
		TCheckbutton(Txt("使用务端提供的文件时间"), UseServerSideTime, Onvalue(1), Offvalue(-1), Command(func() {
			req.UseServerSideTime = UseServerSideTime.Get() == "1"
		})), colSpan(2, Sticky("nw")),
		TCheckbutton(Txt("不合并TS为MP4"), SkipMergeTs, Onvalue(1), Offvalue(-1), Command(func() {
			req.SkipMergeTs = SkipMergeTs.Get() == "1"
		})), colSpan(2, Sticky("nw")),
	)
	WithSkipLog = Variable(0)
	DebugLog = Variable(0)
	row(
		TCheckbutton(Txt("调试日志"), DebugLog, Onvalue(1), Offvalue(-1), Command(func() {
			req.DebugLog = DebugLog.Get() == "1"
		})), colSpan(2, Sticky("nw")),
		TCheckbutton(Txt("记录跳过日志"), WithSkipLog, Onvalue(1), Offvalue(-1), Command(func() {
			req.WithSkipLog = WithSkipLog.Get() == "1"
		})), colSpan(2, Sticky("nw")),
	)
	msg := TLabel(Textvariable("空闲"))
	sv := Variable(0)
	row(msg, colSpan(6, Sticky("nw")))
	row(TProgressbar(Value(0), sv, Length(800)), colSpan(6, Sticky("new")))
	onMerge := false
	onDownload := false
	merge = TButton(Txt("合并TS"), Command(func() {
		msg.Configure(Textvariable("合并点击"))
		if onMerge {
			merge.Configure(Textvariable("合并TS"))
		} else {
			merge.Configure(Textvariable("取消合并"))
		}
		onMerge = !onMerge
	}))
	download = TButton(Txt("开始下载"), Command(func() {
		if onDownload {
			if ctxCC != nil {
				ctxCC()
				ctx = nil
				ctxCC = nil
			}
			m3u8dcpp.CloseOldEnv()
			msg.Configure(Textvariable("空闲"))
			download.Configure(Textvariable("开始下载"))
		} else {
			if ctxCC != nil {
				ctxCC()
				ctx = nil
				ctxCC = nil
			}
			if !syncRequest(false) {
				return
			}
			ctx, ctxCC = context.WithCancel(context.Background())
			m3u8dcpp.StartDownload(req)
			waitingDownload(ctx, time.Millisecond*800, func(s m3u8d.GetStatus_Resp) bool {
				if renderLock.TryLock() {
					render = append(render, func() {
						sv.Set(s.Percent)
						msg.Configure(Textvariable("(" + s.Title + ")" + s.StatusBar))
					})
					renderLock.Unlock()
				}
				if s.ErrMsg != "" {
					ctxCC()
					ctx = nil
					ctxCC = nil
					onDownload = false
					m3u8dcpp.CloseOldEnv()
					defer func() {
						for !renderLock.TryLock() {
						}
						defer renderLock.Unlock()
						if len(render) > 10 {
							render = render[len(render)-10:]
						}
						render = append(render, func() {
							download.Configure(Textvariable("开始下载"))
							msg.Configure(Textvariable("空闲"))
							MessageBox(Default("ok"), Title("错误"), Detail("下载出错:"+s.ErrMsg), Parent(App), Type("ok"))
						})
					}()
					return true
				} else if s.IsCancel || !s.IsDownloading {
					ctxCC()
					ctx = nil
					ctxCC = nil
					onDownload = false
					m3u8dcpp.CloseOldEnv()
					defer func() {
						for !renderLock.TryLock() {

						}
						defer renderLock.Unlock()
						if len(render) > 10 {
							render = render[len(render)-10:]
						}
						render = append(render, func() {
							download.Configure(Textvariable("开始下载"))
							msg.Configure(Textvariable("空闲"))
						})
					}()
					return true
				}
				return false
			})
			download.Configure(Textvariable("取消下载"))
		}
		onDownload = !onDownload
	}))
	row(
		colSpan(2),
		merge, colSpan(2),
		download, colSpan(2),
	)
	syncGUI()
	fn.Panic(ActivateTheme("azure light"))
	WmProtocol(App, "WM_DELETE_WINDOW", Command(func(e *Event) {
		if ctxCC != nil {
			ctxCC()
			ctx = nil
			ctxCC = nil
			if onDownload {
				m3u8dcpp.CloseOldEnv()
			}
		}
		saveConfig()
		os.Exit(0)
	}))
	App.SetResizable(false, false)
	_, _ = NewTicker(time.Millisecond*500, func() {
		if len(render) > 0 {
			renderLock.RLock()
			defer renderLock.RUnlock()
			for _, f := range render {
				f()
			}
		}
	})
	App.IconPhoto(NewPhoto(iconData))
	App.Center().Wait()
}
func truncate(v string) string {
	var i = strings.IndexByte(v, '.')
	if i < 0 {
		return v
	}
	return v[0:i]
}
func waitingDownload(ctx context.Context, d time.Duration, act func(m3u8d.GetStatus_Resp) bool) {
	go func() {
		tk := time.NewTicker(d)
	f:
		for {
			select {
			case <-tk.C:
				if act(m3u8dcpp.GetStatus()) {
					break
				}
			case <-ctx.Done():
				break f
			}
		}
		tk.Stop()
	}()
}

type CurlDialog struct {
	data         *CurlData
	win          *ToplevelWidget
	code         *TextWidget
	buttonFrame  *TFrameWidget
	okButton     *TButtonWidget
	cancelButton *TButtonWidget
}

func (s *CurlDialog) onOk() {
	s.data.Text = s.code.Text()
	s.data.Ok = true
	Destroy(s.win)
}

func (s *CurlDialog) onCancel() { Destroy(s.win) }

func (s *CurlDialog) ShowModal() {
	s.win.Raise(App)
	Focus(s.win)
	Focus(s.code)
	GrabSet(s.win)
	s.win.Center().Wait()
}

var iconData = Data(ico)

type CurlData struct {
	Ok   bool
	Text string
}

func curlDialog(data *CurlData) *CurlDialog {
	dlg := &CurlDialog{data: data}
	dlg.win = App.Toplevel()
	dlg.win.SetResizable(false, false)
	dlg.win.IconPhoto(NewPhoto(iconData))
	dlg.win.WmTitle("curl模式")
	WmProtocol(dlg.win.Window, WM_DELETE_WINDOW, dlg.onCancel)
	dlg.code = dlg.win.Text()
	dlg.code.Insert("1.0", data.Text)
	dlg.buttonFrame = dlg.win.TFrame()
	dlg.okButton = dlg.buttonFrame.TButton(Txt("确定"),
		Command(dlg.onOk))
	dlg.cancelButton = dlg.buttonFrame.TButton(Txt("取消"),
		Command(dlg.onCancel))
	opts := Opts{Padx(3), Pady(3)}
	Grid(dlg.code, Row(0), Column(1), Sticky(WE), opts)
	Grid(dlg.buttonFrame, Row(1), Column(0), Columnspan(2),
		opts)
	Grid(dlg.okButton, Row(0), Column(0), Sticky(E), opts)
	Grid(dlg.cancelButton, Row(0), Column(1), Sticky(E),
		opts)
	GridColumnConfigure(dlg.win, 1, Weight(1))
	return dlg

}

res generate

package main

import (
	"bytes"
	_ "embed"
	"github.com/tc-hib/winres"
	"github.com/tc-hib/winres/version"
	"os"
	"strconv"
	"testing"
	"time"
)

//go:embed favicon.ico
var qs []byte

func rsrc() {
	//版本信息
	rs := winres.ResourceSet{}
	icon, _ := winres.LoadICO(bytes.NewReader(qs))
	rs.SetIcon(winres.Name("APPICON"), icon)
	vi := version.Info{
		FileVersion:    [4]uint16{1, 0, 0, 1},
		ProductVersion: [4]uint16{1, 0, 0, 1},
	}
	year := strconv.Itoa(time.Now().Year())
	var lid uint16
	lid = 0x804                                           //繁中0x404 英文 0x409
	vi.Set(lid, version.ProductName, "xvgui for windows") //产品名称
	vi.Set(lid, version.ProductVersion, "1.0.1")
	vi.Set(lid, version.CompanyName, "Yoby")         //公司
	vi.Set(lid, version.FileDescription, "xv的GUI工具") //产品描述
	vi.Set(lid, version.LegalCopyright, "Copyright©2023-"+year+" Yoby")
	vi.Set(lid, version.OriginalFilename, "xvgui.exe")
	vi.Set(lid, version.LegalTrademarks, "Yoby") //商标
	rs.SetVersionInfo(vi)
	rs.SetManifest(winres.AppManifest{
		ExecutionLevel:      2,
		DPIAwareness:        3,
		UseCommonControlsV6: true,
	})
	out, _ := os.Create("rsrc_amd64.syso")
	defer out.Close()
	rs.WriteObject(out, winres.ArchAMD64)
}

func TestRSRC(t *testing.T) {
	rsrc()
}

build code

go build -o m3u8d.exe --ldflags="-s -w -H windowsgui"

About ~ 14MB for final executable

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions