298 Commits

Author SHA1 Message Date
Aynakeya
6b4fbf6951 Merge pull request #56 from AynaLivePlayer/dev
wxlogin for qqmusic
2025-09-29 23:24:54 +08:00
aynakeya
a81eb4a131 update qqmusic wxlogin 2025-09-29 23:23:23 +08:00
aynakeya
f13577f890 upgrade event to eventbus; remove old event package 2025-09-29 00:02:49 +08:00
aynakeya
82662c308a update fyne version 2025-09-28 22:59:08 +08:00
Aynakeya
eb564402ce Merge pull request #54 from AynaLivePlayer/dev
merge dev
2025-09-14 00:23:34 +08:00
aynakeya
f6f306edc3 update miaosic dependency 2025-09-14 00:22:28 +08:00
aynakeya
a26b58b083 new event bus 2025-09-02 14:31:17 +08:00
Aynakeya
d85ea3aaa2 Merge pull request #53 from AynaLivePlayer/dev
1.2.2 release
2025-08-19 20:53:12 +08:00
aynakeya
093c10092b update version 2025-08-19 20:52:34 +08:00
aynakeya
4f53f870f3 update anyway, fix refresh token logic 2025-08-19 20:51:20 +08:00
aynakeya
5a699a1e2e use state machine to manage player state 2025-08-07 01:09:07 +08:00
Aynakeya
7f6a6e7af5 Merge pull request #52 from AynaLivePlayer/dev
fanle
2025-08-01 21:32:25 +08:00
aynakeya
3aebdb00f9 fanle 2025-08-01 21:31:02 +08:00
Aynakeya
0b50f45f98 Merge pull request #51 from AynaLivePlayer/dev
add dev version
2025-08-01 21:15:47 +08:00
aynakeya
575e1863fd add dev version 2025-08-01 21:15:10 +08:00
Aynakeya
59f0d3f69f Merge pull request #50 from AynaLivePlayer/dev
fix workflow
2025-07-31 01:42:29 +08:00
Aynakeya
1e538646f4 Merge branch 'master' into dev 2025-07-31 01:42:19 +08:00
aynakeya
5c016ae145 fix workflow 2025-07-31 01:40:50 +08:00
Aynakeya
162779f25e Merge pull request #49 from AynaLivePlayer/dev
fix workflow
2025-07-31 00:48:05 +08:00
aynakeya
d2c89b031f fix workflow 2025-07-31 00:47:32 +08:00
Aynakeya
ccbe2117e0 Merge pull request #48 from AynaLivePlayer/dev
1.2.1
2025-07-31 00:33:39 +08:00
aynakeya
466e4a761e add nosource 2025-07-31 00:32:22 +08:00
Aynakeya
07d0a5debc Merge pull request #47 from AynaLivePlayer/dev
update miaosic, remove default source
2025-07-30 22:49:12 +08:00
aynakeya
7e680bc6bf update miaosic, remove default source 2025-07-30 22:48:21 +08:00
Aynakeya
45878dfe99 Merge pull request #46 from AynaLivePlayer/dev
add distribution channel to title
2025-07-30 22:45:30 +08:00
aynakeya
b136a374c4 add distribution channel to title 2025-07-30 22:45:05 +08:00
Aynakeya
6bf17a0dcb Merge pull request #45 from AynaLivePlayer/dev
1.2.0 release
2025-07-30 22:39:11 +08:00
aynakeya
da4ae97923 update todo.txt 2025-07-30 22:37:44 +08:00
aynakeya
2bab6044bf delete default music sources 2025-07-30 22:35:37 +08:00
Aynakeya
2cf80698d9 Merge pull request #44 from AynaLivePlayer/dev
Dev
2025-07-27 12:12:21 +08:00
aynakeya
4ac75dd882 run workflow on master 2025-07-27 12:11:58 +08:00
aynakeya
6af984cfbb fix linux fyne install command & fix window env
try fix windows

try fix windows

fix again

add windows debug build

try ucrt

fix ucrt env

use different mpv build

aaaa

bbb

disable vlc for now

test

build mpv only

update vlc version for windows

add go.sum
2025-07-27 12:10:42 +08:00
aynakeya
82ced0b9a9 test workflow 2025-07-27 03:54:26 +08:00
aynakeya
d11a5a5f76 capitalized first character in all wshub event key 2025-07-24 18:47:24 +08:00
Aynakeya
df771ed9f8 Merge pull request #43 from AynaLivePlayer/dev
fix netease login
2025-07-24 15:31:45 +08:00
aynakeya
3bd8945247 Merge branch 'master' into dev 2025-07-24 15:31:04 +08:00
aynakeya
c31342c708 fix netease login failed/get url failure 2025-07-23 02:44:04 +08:00
aynakeya
46ea1968b6 downgrade fyne to v2.5.5 2025-07-17 00:28:12 +08:00
aynakeya
d4f0c3438c update dependency 2025-07-17 00:28:12 +08:00
aynakeya
1a0e053377 update source 2025-07-17 00:28:12 +08:00
aynakeya
7b87efb076 save session when application close 2025-07-17 00:28:12 +08:00
aynakeya
3f5623f117 downgrade fyne to v2.5.5 2025-07-16 22:43:11 +08:00
aynakeya
261166e67d update dependency 2025-07-16 19:39:08 +08:00
aynakeya
dc727935a8 update source 2025-07-16 19:21:54 +08:00
aynakeya
68a172aa4f save session when application close 2025-07-08 00:33:11 +08:00
Aynakeya
bab3a14d2f Merge pull request #41 from AynaLivePlayer/dev
1.2.0
2025-07-08 00:09:30 +08:00
aynakeya
ee99bd939e Merge branch 'master' into dev 2025-07-08 00:08:00 +08:00
aynakeya
0934bd2d45 update miaosic dependency 2025-07-07 23:29:36 +08:00
aynakeya
7de62ef6ba update miaosic 2025-07-07 00:52:03 +08:00
aynakeya
af6ef96754 revert change 2025-07-07 00:52:03 +08:00
aynakeya
09bacbf7da add qq music 2025-07-07 00:52:03 +08:00
aynakeya
21fe844b5f 1.2.0 feature: qqmusic 2025-07-07 00:52:03 +08:00
aynakeya
72bdef6a91 update go-mpv version 2025-07-07 00:52:03 +08:00
aynakeya
c554f1effc update miaosic 2025-07-07 00:27:39 +08:00
aynakeya
8244380a6a revert change 2025-07-07 00:24:58 +08:00
aynakeya
0bbb9f378f add qq music 2025-07-07 00:18:01 +08:00
aynakeya
eafb074398 1.2.0 feature: qqmusic 2025-07-07 00:14:28 +08:00
aynakeya
7661e966be update go-mpv version 2025-07-02 23:54:34 +08:00
Aynakeya
06df1a1cc4 Merge pull request #39 from AynaLivePlayer/dev
1.2.0
2025-07-02 23:39:49 +08:00
aynakeya
5b4104664e update todo 2025-06-30 04:19:08 +08:00
aynakeya
2b99f7b23f add vlc output device selection method 2025-06-30 04:13:10 +08:00
aynakeya
3b58f6f972 able to switch vlc 2025-06-30 03:48:50 +08:00
aynakeya
374be8ef03 add vlc core 2025-06-30 03:40:33 +08:00
aynakeya
f4aa9b4ac9 fix bug: when diange limit is 1, user can only diange once & update version number to 1.2.0 2025-06-30 00:38:21 +08:00
aynakeya
1fa82c2a01 change config UseSystemFonts to CustomFonts, allow user to specify which font to use 2025-06-30 00:21:41 +08:00
aynakeya
cdc3c5d118 update dependency 2025-06-30 00:17:15 +08:00
aynakeya
c7b70740f6 fix program not closed after hit close button 2025-06-15 00:29:16 +08:00
aynakeya
dd71c3b9ba fyne 2.6.1 update, make all gui threadsafe 2025-06-15 00:04:13 +08:00
aynakeya
45e4c15b8d use config to decide if use system font or not 2025-06-15 00:00:19 +08:00
aynakeya
da81d43584 add use system font config 2025-06-14 23:57:59 +08:00
aynakeya
82df68fa8a update todo 2025-06-05 23:56:32 +08:00
Aynakeya
f897dd0800 Merge pull request #37 from AynaLivePlayer/dev
1.1.3 release
2025-05-28 20:38:16 +08:00
aynakeya
0ea07dc295 update version 2025-05-28 20:38:01 +08:00
aynakeya
edf3b679cc update log 2025-05-27 22:53:57 +08:00
aynakeya
041b678859 live blivedmgo using wbi 2025-05-27 22:44:39 +08:00
aynakeya
1b58c7bc08 fix host zero length 2025-05-27 22:16:56 +08:00
Aynakeya
826e868916 Merge pull request #36 from AynaLivePlayer/dev
1.1.2 fix: bilibili collection format. kugou format
2025-04-09 21:44:39 +08:00
aynakeya
32eab60cad update todo 2025-03-27 22:41:47 +08:00
aynakeya
83fbefaf68 add new bili video collection url format 2025-03-27 22:41:14 +08:00
Aynakeya
f62e9ea43a Merge pull request #35 1.1.2
1.1.2 update
2025-03-23 19:25:26 +08:00
aynakeya
3456bfcb8c update version 2025-03-23 19:23:39 +08:00
aynakeya
df614a16f6 update kugou new playlist url type 2025-03-22 21:22:25 +08:00
aynakeya
8a96506703 make sure log is deleted 2025-03-09 21:31:32 +08:00
aynakeya
53a6ad4aed fix enabled button not working properly 2025-03-05 20:20:17 +08:00
aynakeya
387e96e27e update log 2025-03-05 20:10:08 +08:00
aynakeya
8da41f81e1 remove go.sum 2025-03-05 20:07:42 +08:00
aynakeya
42c80f3ea8 add enabled button 2025-03-05 20:07:17 +08:00
Aynakeya
e311c40f5e Merge pull request #33 from xuanerwa/dev
 Feat: add `yinliang` plugin
2025-03-05 20:03:38 +08:00
xuanerwa
cbba7dd1b3 Merge branch 'AynaLivePlayer:dev' into dev 2025-03-04 17:39:11 +08:00
Aynakeya
e598ed676c Merge pull request #34 from AynaLivePlayer/dev
Fix kugou playlist & fix bili-video search
2025-02-28 11:03:14 +08:00
aynakeya
62a122a9e5 fix kugou playlist 2025-02-28 11:01:55 +08:00
aynakeya
98028885b8 fix bilivideo 2025-02-28 10:14:41 +08:00
xuanerwa
64c7780b60 Feat: add yinliang plugin 2025-02-27 11:10:36 +08:00
aynakeya
d40ef5d79d add default config for kugou-instr 2025-02-21 21:20:04 +08:00
Aynakeya
32e688bb91 Merge pull request #29 upgrade to fyne 2.5.4
upgrade to fyne 2.5.4
2025-02-19 19:29:15 +08:00
aynakeya
df501be65a upgrade to fyne 2.5.4 2025-02-19 19:28:44 +08:00
Aynakeya
fdf12bde26 Merge pull request #28 from AynaLivePlayer/dev
update version
2025-02-18 16:14:14 -08:00
aynakeya
cbde61d99b update version 2025-02-18 16:09:45 -08:00
Aynakeya
2b300af7b7 Merge pull request #27 from AynaLivePlayer/dev
update workflow
2025-02-18 16:08:46 -08:00
aynakeya
1bbaf478bb update workflow 2025-02-18 16:08:07 -08:00
Aynakeya
72589a5eaa Merge pull request #26 from AynaLivePlayer/dev v1.1.1
v1.1.1
2025-02-18 16:05:27 -08:00
aynakeya
0424513d86 1.1.1 2025-02-18 16:04:06 -08:00
aynakeya
f7b6a5eafb update kugou source 2025-02-18 15:59:43 -08:00
aynakeya
1c5e85010b update kugou source 2024-12-06 03:05:59 -08:00
aynakeya
d9581c0bab update kugou source 2024-12-06 01:39:51 -08:00
Aynakeya
8114e69b9f Merge pull request #24 from AynaLivePlayer/dev
add kugou instrumental source
2024-12-03 17:05:59 -08:00
aynakeya
b178f729a2 add kugou instr source 2024-11-17 07:16:52 -08:00
Aynakeya
3d5286b319 Merge pull request #23 from AynaLivePlayer/dev
Add headless mode
2024-11-12 17:02:30 -08:00
aynakeya
5afbe695f2 update git ignore 2024-11-12 17:00:28 -08:00
aynakeya
f36f56cd80 add headless mode 2024-11-12 16:59:36 -08:00
aynakeya
97faa1a538 fix diange command 2024-11-08 16:07:34 -08:00
aynakeya
93e8d4d846 delete unused 2024-11-08 05:28:13 -08:00
aynakeya
4e3dd6400f fix typo 2024-10-24 04:16:31 -07:00
Aynakeya
27a2f1f3a8 Merge pull request #22 from AynaLivePlayer/dev
1.1.0 pre-release.
2024-10-24 00:55:44 -07:00
aynakeya
bc85f86f98 update todo 2024-10-24 00:52:43 -07:00
aynakeya
c1940c0a98 update version number 2024-10-24 00:51:34 -07:00
aynakeya
4e6659935e update timeline props. although windows are not display it 2024-10-24 00:51:23 -07:00
aynakeya
9941fc6993 fix random next 2024-10-23 22:14:57 -07:00
aynakeya
e7c499ca7f finish windows system media control 2024-10-05 21:17:39 -07:00
aynakeya
e61f096bb2 update update log.txt 2024-09-26 00:03:29 -07:00
aynakeya
c1c230a5f0 update dependency: fix medal level for webdm 2024-09-25 23:51:26 -07:00
aynakeya
a2420db1d9 update dependency 2024-09-21 20:14:52 -07:00
aynakeya
0e4a429a19 update dependency 2024-09-02 22:36:32 -07:00
aynakeya
d69f953508 update todo 2024-09-01 20:35:13 -07:00
Aynakeya
04406bf006 Merge pull request #21 from AynaLivePlayer/dev
merge v1.0.9
2024-08-30 23:45:37 -07:00
aynakeya
b5c2a0ce96 1.0.9 release 2024-08-30 23:45:01 -07:00
aynakeya
13ffb609a9 update icon 2024-08-29 21:35:52 -07:00
Aynakeya
2e165d5d0e Merge pull request #20 from AynaLivePlayer/dev
1.0.9 prerelease1
2024-08-29 21:33:15 -07:00
aynakeya
0fe77febdf update ws obsinfo hint text 2024-08-29 21:23:36 -07:00
aynakeya
539e8eabe3 add configure to prevent wshub event execution. 2024-08-27 17:18:12 -07:00
aynakeya
7960299f09 update fyne to 2.5.1 2024-08-26 17:15:31 -07:00
aynakeya
e7f7ddfe4a maybe fix panic when close the software 2024-08-25 10:43:15 -07:00
aynakeya
23d6944a52 add usesysplaylist option 2024-08-25 10:42:18 -07:00
Aynakeya
ef994defb9 Merge pull request #19 from AynaLivePlayer/dev
1.0.9 feature finsh
2024-08-08 23:46:05 +08:00
aynakeya
2056bd310a update dependency 2024-08-08 23:42:00 +08:00
aynakeya
46c2e2710e temporary disable 2.5.0 entry redo/undo 2024-08-07 21:42:31 +08:00
aynakeya
156901f14c fix openblive admin not able qiege 2024-08-07 21:01:30 +08:00
aynakeya
9f8b103be1 fix cover url start with number not able to load 2024-07-24 23:39:35 +08:00
Aynakeya
25bc0f01ca Merge pull request #18 from AynaLivePlayer/dev
fyne update. able to choose to display cover or not
2024-07-24 20:27:10 +08:00
aynakeya
c5d3fb407b fyne update. able to choose to display cover or not 2024-07-24 20:24:42 +08:00
Aynakeya
5d507598e6 Merge pull request #17 from AynaLivePlayer/dev
upgrade fyne to 2.5.0
2024-07-20 21:39:19 +08:00
aynakeya
5480ebdd86 upgrade fyne to 2.5.0 2024-07-20 20:29:59 +08:00
Aynakeya
0e3527c166 Merge pull request #16 from AynaLivePlayer/dev
1.0.8prerelease3
2024-06-23 23:54:15 +08:00
aynakeya
48e5d2f516 critical: fix favlist page not increment 2024-06-23 23:53:20 +08:00
Aynakeya
b7f10431d6 Merge pull request #15 from AynaLivePlayer/dev
1.0.8prelease2
2024-06-23 15:24:43 +08:00
aynakeya
0c976acf80 update todo.txt 2024-06-23 15:24:02 +08:00
aynakeya
a06412e833 update single user max diange count 2024-06-23 15:22:57 +08:00
aynakeya
7e089913be add diange kugou default config 2024-06-23 00:03:26 +08:00
Aynakeya
61596a5b79 Merge pull request #14 from AynaLivePlayer/dev
1.0.8 prerelease
2024-06-22 23:51:42 +08:00
aynakeya
e44285841f update todo 2024-06-22 16:25:34 +08:00
aynakeya
93b3bbbb1f update kugou provider 2024-06-22 15:57:07 +08:00
Aynakeya
29509af04e Merge pull request #13 from AynaLivePlayer/dev
ready for 1.0.8
2024-06-22 10:40:52 +08:00
aynakeya
6073719d69 add multiple diange command handling 2024-06-22 10:21:13 +08:00
aynakeya
c9d84b1134 update go mod 2024-06-22 10:04:10 +08:00
aynakeya
4a76cbdb8b update bilivideo music provider 2024-06-21 23:23:38 +08:00
aynakeya
4cf79a0ab9 ignore smtc for now 2024-06-19 15:55:53 +08:00
aynakeya
56b484257b update workflow 2024-06-11 08:16:21 +08:00
aynakeya
e8953f1e56 update dependency 2024-06-08 22:01:17 +08:00
Aynakeya
1aace54b92 Merge pull request #12 from AynaLivePlayer/dev
fix kuwo. fix local lyric.
2024-06-08 14:14:32 +08:00
aynakeya
0e64d1dc91 update 2024-06-08 14:00:39 +08:00
aynakeya
ea05fcd902 update miaosic source dependency 2024-06-04 19:48:51 +08:00
Aynakeya
afa2729282 Merge pull request #11 from AynaLivePlayer/dev
Merge 1.0.7
2024-05-23 22:02:26 +08:00
aynakeya
36cdfd0824 only on master 2024-05-23 22:01:39 +08:00
aynakeya
ce042bad0a fix build 2024-05-23 21:54:04 +08:00
aynakeya
3959eac396 fix upload executable 2024-05-23 21:24:59 +08:00
aynakeya
2ab27b1b57 uw 2024-05-23 21:12:18 +08:00
aynakeya
7316818093 uw 2024-05-23 21:08:24 +08:00
aynakeya
fb43dcab5d separate jobs 2024-05-23 21:05:12 +08:00
aynakeya
fa7448e2c5 test 2024-05-23 21:00:17 +08:00
aynakeya
5d002becc4 fff 2024-05-23 20:46:02 +08:00
aynakeya
a2d674666a asdf 2024-05-23 20:32:13 +08:00
aynakeya
a6cdf3c903 hello 2024-05-23 20:23:57 +08:00
aynakeya
a892ec3043 cnm 2024-05-23 20:16:17 +08:00
aynakeya
2bc19e2e0c asdf 2024-05-23 20:10:38 +08:00
aynakeya
552f115a33 aaa 2024-05-23 20:03:59 +08:00
aynakeya
92376bdd12 sb 2024-05-23 19:57:02 +08:00
aynakeya
9142d74f19 asdf 2024-05-23 19:29:47 +08:00
aynakeya
65aba16f44 update 2024-05-23 19:23:26 +08:00
aynakeya
37f0dadd8b asdf 2024-05-23 19:16:42 +08:00
aynakeya
c57b7c992f asdf 2024-05-23 18:48:44 +08:00
aynakeya
e47b48bb42 ad 2024-05-23 18:45:47 +08:00
aynakeya
deec6157b9 adsf 2024-05-23 18:37:44 +08:00
aynakeya
7634bd5864 fi 2024-05-23 18:31:52 +08:00
aynakeya
50d3172816 fu 2024-05-23 18:29:49 +08:00
aynakeya
e466e21bc8 fk 2024-05-23 18:27:04 +08:00
aynakeya
7a57a1dcc0 uw 2024-05-23 18:20:27 +08:00
aynakeya
627e5ff9bc uw 2024-05-23 18:14:47 +08:00
aynakeya
ce1322fbde uw 2024-05-23 13:37:21 +08:00
aynakeya
8db6f49c7b uw 2024-05-23 12:55:37 +08:00
aynakeya
161920686f uw 2024-05-23 12:43:04 +08:00
aynakeya
6b595e453b uw 2024-05-22 21:50:17 +08:00
aynakeya
36df238ac1 uw 2024-05-22 21:32:57 +08:00
aynakeya
92f36057a4 uw 2024-05-22 21:09:55 +08:00
aynakeya
b175efa6e3 uw 2024-05-22 20:58:28 +08:00
aynakeya
4aa5071699 uw 2024-05-22 20:53:16 +08:00
aynakeya
318e062198 uw 2024-05-22 20:26:32 +08:00
aynakeya
16c9d1c10e uw 2024-05-22 20:18:14 +08:00
aynakeya
23d59417d5 uw 2024-05-22 20:09:41 +08:00
aynakeya
c6a8a2b272 uw 2024-05-22 20:03:18 +08:00
aynakeya
61342ebfa2 uw 2024-05-22 20:01:29 +08:00
aynakeya
1df1f3a609 uw 2024-05-22 19:59:10 +08:00
aynakeya
891f3d2879 update workflow 2024-05-22 19:56:49 +08:00
aynakeya
0dbb9976d7 update workflow 2024-05-22 19:46:46 +08:00
aynakeya
eabd9fcea3 update workflow 2024-05-22 19:41:14 +08:00
aynakeya
ec110cca9c update workflow 2024-05-22 19:36:47 +08:00
aynakeya
f6fd6d362b update workflow 2024-05-22 19:33:00 +08:00
aynakeya
6ec3987b50 update workflow 2024-05-22 19:29:36 +08:00
aynakeya
812dee0145 fix workflow 2024-05-22 19:23:45 +08:00
aynakeya
16f0aa34a2 fix typo 2024-05-22 19:22:47 +08:00
aynakeya
03862237b4 upload workflow 2024-05-22 19:21:15 +08:00
aynakeya
05322ab0b3 add backward compatibility for mpv client version 1.109 2024-05-22 01:30:12 +08:00
aynakeya
f97c460c46 update 2024-05-22 01:01:36 +08:00
aynakeya
8d24ac8cba update error info 2024-05-22 00:26:36 +08:00
aynakeya
f19babe30a add gui video player window close event 2024-05-21 11:23:56 +08:00
aynakeya
9b55e42811 update mpv 2024-05-21 11:14:15 +08:00
aynakeya
963dedbe65 add cover to video channel 2024-05-19 22:53:41 +08:00
aynakeya
b031bdd3df update dependency 2024-05-16 22:32:02 +08:00
aynakeya
aeca816774 fix comment 2024-05-16 11:34:20 +08:00
aynakeya
9b6f681d4a add event cache. sending all cached event when new websocket connection establish 2024-05-14 22:28:15 +08:00
aynakeya
46ea45580c update lyric 2024-05-11 11:02:47 +08:00
aynakeya
da96f711ae update dependency: fix bilivideo search issue 2024-05-10 10:26:58 +08:00
aynakeya
399f09ba9f update dependency: fix bilivideo issue 2024-05-10 10:21:41 +08:00
aynakeya
2326ef6955 add qiege logging. update dependency 2024-05-07 19:41:40 +08:00
Aynakeya
23db890d47 Merge pull request #9 from AynaLivePlayer/dev
merge 1.0.6
2024-05-06 09:50:38 +08:00
aynakeya
5145680a04 Merge branch 'master' into dev 2024-05-06 09:49:36 +08:00
aynakeya
d12a0155b0 fix todo note 2024-05-06 09:43:35 +08:00
aynakeya
ac8633b4a7 add comment 2024-05-06 08:23:44 +08:00
aynakeya
9b9895e654 update gitignore 2024-05-06 08:19:43 +08:00
aynakeya
f54e01f7ea fix image loading error causing crash. add todo.txt 2024-05-06 08:19:06 +08:00
aynakeya
f24b3e73fb add updater 2024-05-06 08:17:30 +08:00
aynakeya
68c7c591ff update wshub 2024-04-30 02:01:05 -07:00
aynakeya
95a0a97264 change log warn to error 2024-04-29 23:03:16 -07:00
aynakeya
e5076667db fix blacklist not working with id diange 2024-04-29 23:03:04 -07:00
aynakeya
726ac8b449 update dep 2024-04-29 22:51:58 -07:00
aynakeya
88066bd3b9 update wshub 2024-04-29 20:22:35 -07:00
aynakeya
d514f96c28 fix id diange bug 2024-04-28 14:04:44 -07:00
aynakeya
9a277482b4 update todo.txt 2024-04-26 21:19:02 -07:00
aynakeya
39db106a74 update go mod 2024-04-26 21:18:50 -07:00
aynakeya
0e5140f907 set message handle to nil after delete 2024-04-26 21:18:37 -07:00
aynakeya
3786355997 update liveroom-sdk. fix webdanmu missing message after second connection 2024-04-26 21:18:14 -07:00
aynakeya
a5656b8ef0 update version number 2024-04-26 19:48:53 -07:00
aynakeya
884a5afcb5 update to 1.0.5 2024-04-26 19:43:39 -07:00
aynakeya
bec84790fd update dependency 2024-04-26 19:24:05 -07:00
aynakeya
e7775019f7 fix liveroom gui race condition 2024-04-26 18:51:18 -07:00
aynakeya
3fb7941433 update liveroomsdk to fix race condition 2024-04-26 18:50:56 -07:00
aynakeya
b56a1073a6 update liveroomsdk 2024-04-26 18:31:53 -07:00
aynakeya
eba37c04bc raise error when fail to create liveroom 2024-04-26 18:31:40 -07:00
aynakeya
de3d2f6c66 fix random player playlist panic when 0 media in the list 2024-04-26 18:14:09 -07:00
aynakeya
a8091247ec update readme 2024-04-25 20:29:54 -07:00
aynakeya
9c0b711aa0 update dep 2024-04-25 19:56:26 -07:00
aynakeya
58d7ebd43d add skip on fail 2024-04-24 01:27:57 -07:00
aynakeya
ee775dee8d fix translation 2024-04-23 21:29:45 -07:00
aynakeya
5d27040c8b add log file 2024-04-23 09:29:02 -07:00
aynakeya
2169817afa update version 2024-04-22 22:29:14 -07:00
aynakeya
e9149474de fix playlist index not reset to 0 when switch from random 2024-04-22 22:26:42 -07:00
Aynakeya
5cc5948a85 Merge 1.0.x branch (#8)
* rewrite

* update submodule

* make width height configurable

* update dependency

* update

* update file

* update dep

* fix basic config layout

* update plugin management

* more stuff

* add blacklist

* fix todo

* fix windows gethandle

* update windows update guide

* update windows build guide

* include go mod tidy in script

* update todo

* fix source session

* fix text output

* add plugin play duration control

* fix id diange not working

* update todo

* update version number
2024-04-22 21:21:02 -07:00
aynakeya
92ea73ff5a update version number 2024-04-22 20:54:00 -07:00
aynakeya
e567b5b47b update todo 2024-04-22 14:11:44 -07:00
aynakeya
802961576c fix id diange not working 2024-04-22 13:49:17 -07:00
aynakeya
0ad0296e51 add plugin play duration control 2024-04-22 13:49:06 -07:00
aynakeya
f484b28d0e fix text output 2024-04-20 19:14:33 -07:00
aynakeya
e2fa2bdb8e fix source session 2024-04-20 17:31:09 -07:00
aynakeya
e01d067cbc update todo 2024-04-17 12:21:34 -07:00
aynakeya
44203f9887 include go mod tidy in script 2024-04-17 12:17:14 -07:00
aynakeya
ccb3ccbc00 update windows build guide 2024-04-17 12:16:22 -07:00
aynakeya
d64cac9b1b update windows update guide 2024-04-17 12:15:30 -07:00
aynakeya
6ef4ba3fe8 fix windows gethandle 2024-04-17 11:47:28 -07:00
aynakeya
d30db8a95a fix todo 2024-04-15 17:26:19 -07:00
aynakeya
30a2f9ebea add blacklist 2024-04-15 17:18:03 -07:00
aynakeya
6ddcb507f5 more stuff 2024-04-15 16:07:31 -07:00
aynakeya
fbf8c7f149 update plugin management 2024-04-15 14:00:48 -07:00
aynakeya
139f331a14 fix basic config layout 2024-04-15 10:04:27 -07:00
aynakeya
c9cd78b0eb update dep 2024-04-14 09:39:25 -07:00
aynakeya
e7325b6383 update file 2024-04-13 01:00:28 -07:00
aynakeya
fd872c1f5b update 2024-04-13 00:59:44 -07:00
aynakeya
119862a023 update dependency 2024-04-10 01:00:31 -07:00
aynakeya
d52f97429d make width height configurable 2024-04-10 00:52:59 -07:00
aynakeya
084050fcf7 update submodule 2024-04-10 00:44:27 -07:00
aynakeya
f926f15606 rewrite 2024-04-10 00:42:33 -07:00
NearlOfficial
8d73a3c284 Merge pull request #7 from AynaLivePlayer/dev
Temp fix for danmu issue
2023-07-26 23:59:18 +08:00
aynakeya
c993684497 update version number 2023-07-26 12:17:23 +08:00
aynakeya
0ae52e349d tempfix danmu require loginned user 2023-07-26 12:13:54 +08:00
NearlOfficial
cb092366f3 checkUpdateBtn fix bug (#6)
Co-authored-by: NearlOfficial <kamiya814767377@gmail.com>
2023-03-17 03:38:45 -07:00
Aynakeya
18df9ff64c add assets 2023-03-04 19:51:58 -08:00
Aynakeya
02cc9de11a update checker 2023-02-21 16:21:29 -08:00
Aynakeya
6f2349e17b fix gui freeze bug (work round) 2023-02-19 04:23:03 -08:00
Aynakeya
9d99a74faf ui界面优化,event handler优化-新增任务池模式,歌词加载优化,新房间管理(可以自动连接) 本地音频搜索算法优化, 2022-12-31 21:22:35 -08:00
Aynakeya
1e18ca1ff2 ui界面优化,event handler优化-新增任务池模式,歌词加载优化,新房间管理(可以自动连接) 本地音频搜索算法优化, 2022-12-25 01:27:56 -08:00
Aynakeya
9ec4057412 重写controller部分,修改search界面,添加歌词滚动效果,部分资源添加到bundle,修复拖动进度条时产生的噪音 2022-12-24 03:51:21 -08:00
Aynakeya
c47d338a9e rewrite using IoC and DI 2022-12-23 05:06:57 -08:00
Aynakeya
0498d2dbf3 new room gui 2022-11-28 18:39:12 -08:00
Aynakeya
eac8b7b775 local search algorithm optimization/diange medal permission/ 2022-08-24 15:13:13 -07:00
Aynakeya
d20c39ace3 fix config not saving 2022-08-20 10:10:12 -07:00
Aynakeya
a8d5e9d772 0.9.2 2022-08-19 13:32:42 -07:00
Aynakeya
b6645cc575 update dependency. 2022-08-19 13:16:40 -07:00
Aynakeya
4c0b407475 fix playlist gui not refresh when switching 2022-07-21 23:30:28 -07:00
Aynakeya
c0c83ef82a fix local playlist bug/to 0.9.1 2022-07-21 23:20:19 -07:00
Aynakeya
f4b080da25 fix local playlist bug 2022-07-21 23:07:41 -07:00
240 changed files with 11878 additions and 7403 deletions

132
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,132 @@
name: Build
on:
push:
branches:
- master
jobs:
build-windows:
runs-on: windows-latest
timeout-minutes: 20
env:
GOOS: windows
GOARCH: amd64
EXECUTABLE: AynaLivePlayer.exe
CGO_CFLAGS: "-I${{ github.workspace }}/libmpv/include -I${{ github.workspace }}/libvlc/VideoLAN.LibVLC.Windows.3.0.21/build/x64/include"
CGO_LDFLAGS: "-L${{ github.workspace }}/libmpv -L${{ github.workspace }}/libvlc/VideoLAN.LibVLC.Windows.3.0.21/build/x64"
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install 7-Zip
run: choco install 7zip
- name: Download libmpv
uses: robinraju/release-downloader@v1
with:
repository: "shinchiro/mpv-winbuild-cmake"
latest: true
fileName: "mpv-dev-x86_64-[0-9]*.7z"
extract: false
out-file-path: "libmpv"
- name: Extract libmpv
run: |
7z x "libmpv/mpv-dev-x86_64-*.7z" -o"libmpv"
- name: Setup NuGet.exe
uses: nuget/setup-nuget@v2
with:
nuget-version: 'latest'
- name: Install VLC dependency
run: nuget install VideoLAN.LibVLC.Windows -OutputDirectory ${{ github.workspace }}/libvlc -Version 3.0.21
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Install Go Dependencies
run: |
go mod tidy
go install fyne.io/tools/cmd/fyne@latest
- name: Bundle assets
run: |
fyne bundle --name resImageIcon --package resource ./assets/icon2.png > ./resource/bundle.go
- name: Build application
run: |
go build -tags="mpvOnly,nosource" -v -o ./AynaLivePlayerMpvNoSource.exe -ldflags -H=windowsgui app/main.go
go build -tags="vlcOnly,nosource" -v -o ./AynaLivePlayerVlcNoSource.exe -ldflags -H=windowsgui app/main.go
go build -tags="nosource" -v -o ./AynaLivePlayerAllPlayerNoSource.exe -ldflags -H=windowsgui app/main.go
go build -tags="mpvOnly" -v -o ./AynaLivePlayerMpv.exe -ldflags -H=windowsgui app/main.go
go build -tags="vlcOnly" -v -o ./AynaLivePlayerVlc.exe -ldflags -H=windowsgui app/main.go
go build -v -o ./AynaLivePlayerAllPlayer.exe -ldflags -H=windowsgui app/main.go
- name: Upload artifact (NoSource)
uses: actions/upload-artifact@v4
with:
name: windows-build-nosource
path: |
./AynaLivePlayerMpvNoSource.exe
./AynaLivePlayerVlcNoSource.exe
./AynaLivePlayerAllPlayerNoSource.exe
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: windows-build
path: |
./AynaLivePlayerMpv.exe
./AynaLivePlayerVlc.exe
./AynaLivePlayerAllPlayer.exe
build-ubuntu:
runs-on: ubuntu-latest
timeout-minutes: 10
env:
GOOS: linux
GOARCH: amd64
EXECUTABLE: AynaLivePlayer
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y libvlc-dev vlc libmpv-dev libgl-dev libxcursor-dev libxrandr-dev libxinerama-dev libxi-dev libgl1-mesa-dev xorg-dev
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Install Go Dependencies
run: |
go mod tidy
go install fyne.io/tools/cmd/fyne@latest
- name: Bundle assets
run: |
fyne bundle --name resImageIcon --package resource ./assets/icon.png > ./resource/bundle.go
- name: Build application
run: go build -o ./${{ env.EXECUTABLE }} app/main.go
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ubuntu-build
path: ./${{ env.EXECUTABLE }}

11
.gitignore vendored
View File

@@ -1,3 +1,12 @@
.idea
assets/webinfo/*.html
assets/webinfo/assets
assets/webinfo/assets
resource/bundle.go
music
/txtinfo/
CMakeCache.txt
/config/
/release/
log.txt
config.ini
config.ini.bak

6
.gitmodules vendored Normal file
View File

@@ -0,0 +1,6 @@
[submodule "pkg/miaosic"]
path = pkg/miaosic
url = git@github.com:AynaLivePlayer/miaosic.git
[submodule "pkg/liveroom-sdk"]
path = pkg/liveroom-sdk
url = git@github.com:AynaLivePlayer/liveroom-sdk.git

58
Makefile Normal file
View File

@@ -0,0 +1,58 @@
EXECUTABLE=AynaLivePlayer
WINDOWS=$(EXECUTABLE).exe
LINUX=$(EXECUTABLE)_linux
DARWIN=$(EXECUTABLE)_darwin
ifeq ($(OS),Windows_NT)
RM = del /Q /F
RRM = rmdir /Q /S
MKDIR = mkdir
COPY = XCOPY /Y
MOVE = move
else
RM = rm -f
RRM = rm -rf
MKDIR = mkdir
COPY = cp -r
MOVE = mv
endif
bundle:
fyne bundle --name resImageIcon --package resource ./assets/icon.png > ./resource/bundle.go
fyne bundle --append --name resFontMSYaHei --package resource ./assets/msyh0.ttf >> ./resource/bundle.go
fyne bundle --append --name resFontMSYaHeiBold --package resource ./assets/msyhbd0.ttf >> ./resource/bundle.go
# fyne bundle --append --name resFontMSYaHei --package resource ./assets/msyh.ttc >> ./resource/bundle.go
# fyne bundle --append --name resFontMSYaHeiBold --package resource ./assets/msyhbd.ttc >> ./resource/bundle.go
prebuild: bundle
$(RRM) ./release
$(MKDIR) ./release
$(MKDIR) ./release/assets
$(COPY) LICENSE.md ./release/LICENSE.md
$(COPY) ./assets/translation.json ./release/assets/translation.json
$(COPY) ./assets/config ./release/config
$(COPY) ./music ./release/music
go mod tidy
$(LINUX): prebuild
env GOOS=linux GOARCH=amd64 go build -o ./release/$(LINUX) app/main.go
$(MOVE) ./release/$(LINUX) ./release/$(EXECUTABLE)
$(WINDOWS): prebuild
env GOOS=windows GOARCH=amd64 go build -o ./release/$(WINDOWS) -ldflags -H=windowsgui app/main.go
$(DARWIN): prebuild
env GOOS=darwin GOARCH=amd64 go build -o ./release/$(DARWIN) app/main.go
$(MOVE) ./release/$(LINUX) ./release/$(EXECUTABLE)
windows: $(WINDOWS) ## Build for Windows
$(COPY) ./assets/windows/mpv-2.dll ./release/mpv-2.dll
linux: $(LINUX) ## Build for Linux
darwin: $(DARWIN) ## Build for Darwin (macOS)
clean:
$(RRM) ./release

View File

@@ -8,11 +8,50 @@ QQ group: 621035845
## build
> outdated, please refer to workflow file
```
go build -o AynaLivePlayer.exe -ldflags -H=windowsgui app/gui/main.go
```
## packaging
> outdated, please refer to workflow file
```
fyne package --src path_to_gui --exe AynaLivePlayer.exe --appVersion 0.8.4 --icon path_to_icon
```
## Windows build guide
> outdated, please refer to workflow file
1. install golang [link](https://go.dev/doc/install)
2. install chocolatey [link](https://chocolatey.org/install)
3. install required packages
```
choco install git
choco install mingw
```
4. install fyne
```
go install fyne.io/fyne/v2/cmd/fyne@latest
```
5. clone this repo
```bash
git clone --recurse-submodules git@github.com:AynaLivePlayer/AynaLivePlayer.git
```
if you are using https links
```
git clone https://github.com/AynaLivePlayer/AynaLivePlayer.git
git submodule set-url pkg/miaosic https://github.com/AynaLivePlayer/miaosic.git
git submodule set-url pkg/liveroom-sdk https://github.com/AynaLivePlayer/liveroom-sdk.git
git submodule update
```
6. now you can build (please check makefile for more details)
```powershell
$env:CGO_LDFLAGS="-LC:\Users\Admin\Desktop\AynaLivePlayer\libmpv\lib";$env:CGO_CFLAGS="-IC:\Users\Admin\Desktop\AynaLivePlayer\libmpv\include"
# ... more setup, see makefile
go build -o AynaLivePlayer.exe -ldflags -H=windowsgui app/main.go
```

View File

@@ -1,23 +0,0 @@
package main
import (
"AynaLivePlayer/config"
"AynaLivePlayer/controller"
"AynaLivePlayer/logger"
"fmt"
"github.com/sirupsen/logrus"
)
func main() {
fmt.Printf("BiliAudioBot Revive %s\n", config.Version)
logger.Logger.SetLevel(logrus.DebugLevel)
fmt.Println("Please enter room id")
var roomid string
// Taking input from user
fmt.Scanln(&roomid)
controller.Initialize()
controller.SetDanmuClient(roomid)
ch := make(chan int)
<-ch
}

View File

@@ -1,5 +0,0 @@
[Details]
Icon = "../../theme/icons/fyne.png"
Name = "Fyne Demo"
ID = "io.fyne.demo"
Build = 6

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

File diff suppressed because one or more lines are too long

View File

@@ -1,43 +0,0 @@
package data
//go:generate fyne bundle -package data -o bundled.go assets
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/theme"
)
// ThemedResource is a resource wrapper that will return an appropriate resource
// for the currently selected theme.
type ThemedResource struct {
dark, light fyne.Resource
}
func isLight() bool {
r, g, b, _ := theme.ForegroundColor().RGBA()
return r < 0xaaaa && g < 0xaaaa && b < 0xaaaa
}
// Name returns the underlying resource name (used for caching)
func (res *ThemedResource) Name() string {
if isLight() {
return res.light.Name()
}
return res.dark.Name()
}
// Content returns the underlying content of the correct resource for the current theme
func (res *ThemedResource) Content() []byte {
if isLight() {
return res.light.Content()
}
return res.dark.Content()
}
// NewThemedResource creates a resource that adapts to the current theme setting.
func NewThemedResource(dark, light fyne.Resource) *ThemedResource {
return &ThemedResource{dark, light}
}
// FyneScene contains the full fyne logo with background design
var FyneScene = NewThemedResource(resourceFynescenedarkPng, resourceFynescenelightPng)

View File

@@ -1,203 +0,0 @@
// Package main provides various examples of Fyne API capabilities.
package main
import (
"fmt"
"log"
"net/url"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/cmd/fyne_demo/tutorials"
"fyne.io/fyne/v2/cmd/fyne_settings/settings"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
const preferenceCurrentTutorial = "currentTutorial"
var topWindow fyne.Window
func main() {
a := app.NewWithID("io.fyne.demo")
a.SetIcon(theme.FyneLogo())
logLifecycle(a)
w := a.NewWindow("Fyne Demo")
topWindow = w
w.SetMainMenu(makeMenu(a, w))
w.SetMaster()
content := container.NewMax()
title := widget.NewLabel("Component name")
intro := widget.NewLabel("An introduction would probably go\nhere, as well as a")
intro.Wrapping = fyne.TextWrapWord
setTutorial := func(t tutorials.Tutorial) {
if fyne.CurrentDevice().IsMobile() {
child := a.NewWindow(t.Title)
topWindow = child
child.SetContent(t.View(topWindow))
child.Show()
child.SetOnClosed(func() {
topWindow = w
})
return
}
title.SetText(t.Title)
intro.SetText(t.Intro)
content.Objects = []fyne.CanvasObject{t.View(w)}
content.Refresh()
}
tutorial := container.NewBorder(
container.NewVBox(title, widget.NewSeparator(), intro), nil, nil, nil, content)
if fyne.CurrentDevice().IsMobile() {
w.SetContent(makeNav(setTutorial, false))
} else {
split := container.NewHSplit(makeNav(setTutorial, true), tutorial)
split.Offset = 0.2
w.SetContent(split)
}
w.Resize(fyne.NewSize(640, 460))
w.ShowAndRun()
}
func logLifecycle(a fyne.App) {
a.Lifecycle().SetOnStarted(func() {
log.Println("Lifecycle: Started")
})
a.Lifecycle().SetOnStopped(func() {
log.Println("Lifecycle: Stopped")
})
a.Lifecycle().SetOnEnteredForeground(func() {
log.Println("Lifecycle: Entered Foreground")
})
a.Lifecycle().SetOnExitedForeground(func() {
log.Println("Lifecycle: Exited Foreground")
})
}
func makeMenu(a fyne.App, w fyne.Window) *fyne.MainMenu {
newItem := fyne.NewMenuItem("New", nil)
checkedItem := fyne.NewMenuItem("Checked", nil)
checkedItem.Checked = true
disabledItem := fyne.NewMenuItem("Disabled", nil)
disabledItem.Disabled = true
otherItem := fyne.NewMenuItem("Other", nil)
otherItem.ChildMenu = fyne.NewMenu("",
fyne.NewMenuItem("Project", func() { fmt.Println("Menu New->Other->Project") }),
fyne.NewMenuItem("Mail", func() { fmt.Println("Menu New->Other->Mail") }),
)
newItem.ChildMenu = fyne.NewMenu("",
fyne.NewMenuItem("File", func() { fmt.Println("Menu New->File") }),
fyne.NewMenuItem("Directory", func() { fmt.Println("Menu New->Directory") }),
otherItem,
)
settingsItem := fyne.NewMenuItem("Settings", func() {
w := a.NewWindow("Fyne Settings")
w.SetContent(settings.NewSettings().LoadAppearanceScreen(w))
w.Resize(fyne.NewSize(480, 480))
w.Show()
})
cutItem := fyne.NewMenuItem("Cut", func() {
shortcutFocused(&fyne.ShortcutCut{
Clipboard: w.Clipboard(),
}, w)
})
copyItem := fyne.NewMenuItem("Copy", func() {
shortcutFocused(&fyne.ShortcutCopy{
Clipboard: w.Clipboard(),
}, w)
})
pasteItem := fyne.NewMenuItem("Paste", func() {
shortcutFocused(&fyne.ShortcutPaste{
Clipboard: w.Clipboard(),
}, w)
})
findItem := fyne.NewMenuItem("Find", func() { fmt.Println("Menu Find") })
helpMenu := fyne.NewMenu("Help",
fyne.NewMenuItem("Documentation", func() {
u, _ := url.Parse("https://developer.fyne.io")
_ = a.OpenURL(u)
}),
fyne.NewMenuItem("Support", func() {
u, _ := url.Parse("https://fyne.io/support/")
_ = a.OpenURL(u)
}),
fyne.NewMenuItemSeparator(),
fyne.NewMenuItem("Sponsor", func() {
u, _ := url.Parse("https://fyne.io/sponsor/")
_ = a.OpenURL(u)
}))
// a quit item will be appended to our first (File) menu
file := fyne.NewMenu("File", newItem, checkedItem, disabledItem)
if !fyne.CurrentDevice().IsMobile() {
file.Items = append(file.Items, fyne.NewMenuItemSeparator(), settingsItem)
}
return fyne.NewMainMenu(
file,
fyne.NewMenu("Edit", cutItem, copyItem, pasteItem, fyne.NewMenuItemSeparator(), findItem),
helpMenu,
)
}
func makeNav(setTutorial func(tutorial tutorials.Tutorial), loadPrevious bool) fyne.CanvasObject {
a := fyne.CurrentApp()
tree := &widget.Tree{
ChildUIDs: func(uid string) []string {
return tutorials.TutorialIndex[uid]
},
IsBranch: func(uid string) bool {
children, ok := tutorials.TutorialIndex[uid]
return ok && len(children) > 0
},
CreateNode: func(branch bool) fyne.CanvasObject {
return widget.NewLabel("Collection Widgets")
},
UpdateNode: func(uid string, branch bool, obj fyne.CanvasObject) {
t, ok := tutorials.Tutorials[uid]
if !ok {
fyne.LogError("Missing tutorial panel: "+uid, nil)
return
}
obj.(*widget.Label).SetText(t.Title)
},
OnSelected: func(uid string) {
if t, ok := tutorials.Tutorials[uid]; ok {
a.Preferences().SetString(preferenceCurrentTutorial, uid)
setTutorial(t)
}
},
}
if loadPrevious {
currentPref := a.Preferences().StringWithFallback(preferenceCurrentTutorial, "welcome")
tree.Select(currentPref)
}
themes := fyne.NewContainerWithLayout(layout.NewGridLayout(2),
widget.NewButton("Dark", func() {
a.Settings().SetTheme(theme.DarkTheme())
}),
widget.NewButton("Light", func() {
a.Settings().SetTheme(theme.LightTheme())
}),
)
return container.NewBorder(nil, themes, nil, nil, tree)
}
func shortcutFocused(s fyne.Shortcut, w fyne.Window) {
if focused, ok := w.Canvas().Focused().(fyne.Shortcutable); ok {
focused.TypedShortcut(s)
}
}

View File

@@ -1,85 +0,0 @@
package tutorials
import (
"strconv"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/widget"
)
func scaleString(c fyne.Canvas) string {
return strconv.FormatFloat(float64(c.Scale()), 'f', 2, 32)
}
func texScaleString(c fyne.Canvas) string {
pixels, _ := c.PixelCoordinateForPosition(fyne.NewPos(1, 1))
texScale := float32(pixels) / c.Scale()
return strconv.FormatFloat(float64(texScale), 'f', 2, 32)
}
func prependTo(g *fyne.Container, s string) {
g.Objects = append([]fyne.CanvasObject{widget.NewLabel(s)}, g.Objects...)
g.Refresh()
}
func setScaleText(scale, tex *widget.Label, win fyne.Window) {
for scale.Visible() {
scale.SetText(scaleString(win.Canvas()))
tex.SetText(texScaleString(win.Canvas()))
time.Sleep(time.Second)
}
}
// advancedScreen loads a panel that shows details and settings that are a bit
// more detailed than normally needed.
func advancedScreen(win fyne.Window) fyne.CanvasObject {
scale := widget.NewLabel("")
tex := widget.NewLabel("")
screen := widget.NewCard("Screen info", "", widget.NewForm(
&widget.FormItem{Text: "Scale", Widget: scale},
&widget.FormItem{Text: "Texture Scale", Widget: tex},
))
go setScaleText(scale, tex, win)
label := widget.NewLabel("Just type...")
generic := container.NewVBox()
desk := container.NewVBox()
genericCard := widget.NewCard("", "Generic", container.NewVScroll(generic))
deskCard := widget.NewCard("", "Desktop", container.NewVScroll(desk))
win.Canvas().SetOnTypedRune(func(r rune) {
prependTo(generic, "Rune: "+string(r))
})
win.Canvas().SetOnTypedKey(func(ev *fyne.KeyEvent) {
prependTo(generic, "Key : "+string(ev.Name))
})
if deskCanvas, ok := win.Canvas().(desktop.Canvas); ok {
deskCanvas.SetOnKeyDown(func(ev *fyne.KeyEvent) {
prependTo(desk, "KeyDown: "+string(ev.Name))
})
deskCanvas.SetOnKeyUp(func(ev *fyne.KeyEvent) {
prependTo(desk, "KeyUp : "+string(ev.Name))
})
}
return container.NewHBox(
container.NewVBox(screen,
widget.NewButton("Custom Theme", func() {
fyne.CurrentApp().Settings().SetTheme(newCustomTheme())
}),
widget.NewButton("Fullscreen", func() {
win.SetFullScreen(!win.FullScreen())
}),
),
container.NewBorder(label, nil, nil, nil,
container.NewGridWithColumns(2, genericCard, deskCard),
),
)
}

View File

@@ -1,143 +0,0 @@
package tutorials
import (
"image/color"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
func makeAnimationScreen(_ fyne.Window) fyne.CanvasObject {
curves := makeAnimationCurves()
curves.Move(fyne.NewPos(0, 140+theme.Padding()))
return fyne.NewContainerWithoutLayout(makeAnimationCanvas(), curves)
}
func makeAnimationCanvas() fyne.CanvasObject {
rect := canvas.NewRectangle(color.Black)
rect.Resize(fyne.NewSize(410, 140))
a := canvas.NewColorRGBAAnimation(theme.PrimaryColorNamed(theme.ColorBlue), theme.PrimaryColorNamed(theme.ColorGreen),
time.Second*3, func(c color.Color) {
rect.FillColor = c
canvas.Refresh(rect)
})
a.RepeatCount = fyne.AnimationRepeatForever
a.AutoReverse = true
a.Start()
var a2 *fyne.Animation
i := widget.NewIcon(theme.CheckButtonCheckedIcon())
a2 = canvas.NewPositionAnimation(fyne.NewPos(0, 0), fyne.NewPos(350, 80), time.Second*3, func(p fyne.Position) {
i.Move(p)
width := 10 + (p.X / 7)
i.Resize(fyne.NewSize(width, width))
})
a2.RepeatCount = fyne.AnimationRepeatForever
a2.AutoReverse = true
a2.Curve = fyne.AnimationLinear
a2.Start()
running := true
var toggle *widget.Button
toggle = widget.NewButton("Stop", func() {
if running {
a.Stop()
a2.Stop()
toggle.SetText("Start")
} else {
a.Start()
a2.Start()
toggle.SetText("Stop")
}
running = !running
})
toggle.Resize(toggle.MinSize())
toggle.Move(fyne.NewPos(152, 54))
return fyne.NewContainerWithoutLayout(rect, i, toggle)
}
func makeAnimationCurves() fyne.CanvasObject {
label1, box1, a1 := makeAnimationCurveItem("EaseInOut", fyne.AnimationEaseInOut, 0)
label2, box2, a2 := makeAnimationCurveItem("EaseIn", fyne.AnimationEaseIn, 30+theme.Padding())
label3, box3, a3 := makeAnimationCurveItem("EaseOut", fyne.AnimationEaseOut, 60+theme.Padding()*2)
label4, box4, a4 := makeAnimationCurveItem("Linear", fyne.AnimationLinear, 90+theme.Padding()*3)
start := widget.NewButton("Compare", func() {
a1.Start()
a2.Start()
a3.Start()
a4.Start()
})
start.Resize(start.MinSize())
start.Move(fyne.NewPos(0, 120+theme.Padding()*4))
return fyne.NewContainerWithoutLayout(label1, label2, label3, label4, box1, box2, box3, box4, start)
}
func makeAnimationCurveItem(label string, curve fyne.AnimationCurve, yOff float32) (
text *widget.Label, box fyne.CanvasObject, anim *fyne.Animation) {
text = widget.NewLabel(label)
text.Alignment = fyne.TextAlignCenter
text.Resize(fyne.NewSize(380, 30))
text.Move(fyne.NewPos(0, yOff))
box = newThemedBox()
box.Resize(fyne.NewSize(30, 30))
box.Move(fyne.NewPos(0, yOff))
anim = canvas.NewPositionAnimation(
fyne.NewPos(0, yOff), fyne.NewPos(380, yOff), time.Second, func(p fyne.Position) {
box.Move(p)
box.Refresh()
})
anim.Curve = curve
anim.AutoReverse = true
anim.RepeatCount = 1
return
}
// themedBox is a simple box that change its background color according
// to the selected theme
type themedBox struct {
widget.BaseWidget
}
func newThemedBox() *themedBox {
b := &themedBox{}
b.ExtendBaseWidget(b)
return b
}
func (b *themedBox) CreateRenderer() fyne.WidgetRenderer {
b.ExtendBaseWidget(b)
bg := canvas.NewRectangle(theme.ForegroundColor())
return &themedBoxRenderer{bg: bg, objects: []fyne.CanvasObject{bg}}
}
type themedBoxRenderer struct {
bg *canvas.Rectangle
objects []fyne.CanvasObject
}
func (r *themedBoxRenderer) Destroy() {
}
func (r *themedBoxRenderer) Layout(size fyne.Size) {
r.bg.Resize(size)
}
func (r *themedBoxRenderer) MinSize() fyne.Size {
return r.bg.MinSize()
}
func (r *themedBoxRenderer) Objects() []fyne.CanvasObject {
return r.objects
}
func (r *themedBoxRenderer) Refresh() {
r.bg.FillColor = theme.ForegroundColor()
r.bg.Refresh()
}

View File

@@ -1,111 +0,0 @@
package tutorials
import (
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/widget"
)
func bindingScreen(_ fyne.Window) fyne.CanvasObject {
f := 0.2
data := binding.BindFloat(&f)
label := widget.NewLabelWithData(binding.FloatToStringWithFormat(data, "Float value: %0.2f"))
entry := widget.NewEntryWithData(binding.FloatToString(data))
floats := container.NewGridWithColumns(2, label, entry)
slide := widget.NewSliderWithData(0, 1, data)
slide.Step = 0.01
bar := widget.NewProgressBarWithData(data)
buttons := container.NewGridWithColumns(4,
widget.NewButton("0%", func() {
data.Set(0)
}),
widget.NewButton("30%", func() {
data.Set(0.3)
}),
widget.NewButton("70%", func() {
data.Set(0.7)
}),
widget.NewButton("100%", func() {
data.Set(1)
}))
boolData := binding.NewBool()
check := widget.NewCheckWithData("Check me!", boolData)
checkLabel := widget.NewLabelWithData(binding.BoolToString(boolData))
checkEntry := widget.NewEntryWithData(binding.BoolToString(boolData))
checks := container.NewGridWithColumns(3, check, checkLabel, checkEntry)
item := container.NewVBox(floats, slide, bar, buttons, widget.NewSeparator(), checks, widget.NewSeparator())
dataList := binding.BindFloatList(&[]float64{0.1, 0.2, 0.3})
button := widget.NewButton("Append", func() {
dataList.Append(float64(dataList.Length()+1) / 10)
})
list := widget.NewListWithData(dataList,
func() fyne.CanvasObject {
return container.NewBorder(nil, nil, nil, widget.NewButton("+", nil),
widget.NewLabel("item x.y"))
},
func(item binding.DataItem, obj fyne.CanvasObject) {
f := item.(binding.Float)
text := obj.(*fyne.Container).Objects[0].(*widget.Label)
text.Bind(binding.FloatToStringWithFormat(f, "item %0.1f"))
btn := obj.(*fyne.Container).Objects[1].(*widget.Button)
btn.OnTapped = func() {
val, _ := f.Get()
_ = f.Set(val + 1)
}
})
formStruct := struct {
Name, Email string
Subscribe bool
}{}
formData := binding.BindStruct(&formStruct)
form := newFormWithData(formData)
form.OnSubmit = func() {
fmt.Println("Struct:\n", formStruct)
}
listPanel := container.NewBorder(nil, button, nil, nil, list)
return container.NewBorder(item, nil, nil, nil, container.NewGridWithColumns(2, listPanel, form))
}
func newFormWithData(data binding.DataMap) *widget.Form {
keys := data.Keys()
items := make([]*widget.FormItem, len(keys))
for i, k := range keys {
data, err := data.GetItem(k)
if err != nil {
items[i] = widget.NewFormItem(k, widget.NewLabel(err.Error()))
}
items[i] = widget.NewFormItem(k, createBoundItem(data))
}
return widget.NewForm(items...)
}
func createBoundItem(v binding.DataItem) fyne.CanvasObject {
switch val := v.(type) {
case binding.Bool:
return widget.NewCheckWithData("", val)
case binding.Float:
s := widget.NewSliderWithData(0, 1, val)
s.Step = 0.01
return s
case binding.Int:
return widget.NewEntryWithData(binding.IntToString(val))
case binding.String:
return widget.NewEntryWithData(val)
default:
return widget.NewLabel("")
}
}

View File

@@ -1,49 +0,0 @@
package tutorials
import (
"image/color"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
)
func rgbGradient(x, y, w, h int) color.Color {
g := int(float32(x) / float32(w) * float32(255))
b := int(float32(y) / float32(h) * float32(255))
return color.NRGBA{uint8(255 - b), uint8(g), uint8(b), 0xff}
}
// canvasScreen loads a graphics example panel for the demo app
func canvasScreen(_ fyne.Window) fyne.CanvasObject {
gradient := canvas.NewHorizontalGradient(color.NRGBA{0x80, 0, 0, 0xff}, color.NRGBA{0, 0x80, 0, 0xff})
go func() {
for {
time.Sleep(time.Second)
gradient.Angle += 45
if gradient.Angle >= 360 {
gradient.Angle -= 360
}
canvas.Refresh(gradient)
}
}()
return fyne.NewContainerWithLayout(layout.NewGridWrapLayout(fyne.NewSize(90, 90)),
canvas.NewImageFromResource(theme.FyneLogo()),
&canvas.Rectangle{FillColor: color.NRGBA{0x80, 0, 0, 0xff},
StrokeColor: color.NRGBA{0xff, 0xff, 0xff, 0xff},
StrokeWidth: 1},
&canvas.Line{StrokeColor: color.NRGBA{0, 0, 0x80, 0xff}, StrokeWidth: 5},
&canvas.Circle{StrokeColor: color.NRGBA{0, 0, 0x80, 0xff},
FillColor: color.NRGBA{0x30, 0x30, 0x30, 0x60},
StrokeWidth: 2},
canvas.NewText("Text", color.NRGBA{0, 0x80, 0, 0xff}),
canvas.NewRasterWithPixels(rgbGradient),
gradient,
canvas.NewRadialGradient(color.NRGBA{0x80, 0, 0, 0xff}, color.NRGBA{0, 0x80, 0x80, 0xff}),
)
}

View File

@@ -1,119 +0,0 @@
package tutorials
import (
"fmt"
"strconv"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
// collectionScreen loads a tab panel for collection widgets
func collectionScreen(_ fyne.Window) fyne.CanvasObject {
content := container.NewVBox(
widget.NewLabelWithStyle("func Length() int", fyne.TextAlignLeading, fyne.TextStyle{Monospace: true}),
widget.NewLabelWithStyle("func CreateItem() fyne.CanvasObject", fyne.TextAlignLeading, fyne.TextStyle{Monospace: true}),
widget.NewLabelWithStyle("func UpdateItem(ListItemID, fyne.CanvasObject)", fyne.TextAlignLeading, fyne.TextStyle{Monospace: true}),
widget.NewLabelWithStyle("func OnSelected(ListItemID)", fyne.TextAlignLeading, fyne.TextStyle{Monospace: true}),
widget.NewLabelWithStyle("func OnUnselected(ListItemID)", fyne.TextAlignLeading, fyne.TextStyle{Monospace: true}))
return container.NewCenter(content)
}
func makeListTab(_ fyne.Window) fyne.CanvasObject {
data := make([]string, 1000)
for i := range data {
data[i] = "Test Item " + strconv.Itoa(i)
}
icon := widget.NewIcon(nil)
label := widget.NewLabel("Select An Item From The List")
hbox := container.NewHBox(icon, label)
list := widget.NewList(
func() int {
return len(data)
},
func() fyne.CanvasObject {
return container.NewHBox(widget.NewIcon(theme.DocumentIcon()), widget.NewLabel("Template Object"))
},
func(id widget.ListItemID, item fyne.CanvasObject) {
item.(*fyne.Container).Objects[1].(*widget.Label).SetText(data[id])
},
)
list.OnSelected = func(id widget.ListItemID) {
label.SetText(data[id])
icon.SetResource(theme.DocumentIcon())
}
list.OnUnselected = func(id widget.ListItemID) {
label.SetText("Select An Item From The List")
icon.SetResource(nil)
}
list.Select(125)
return container.NewHSplit(list, container.NewCenter(hbox))
}
func makeTableTab(_ fyne.Window) fyne.CanvasObject {
t := widget.NewTable(
func() (int, int) { return 500, 150 },
func() fyne.CanvasObject {
return widget.NewLabel("Cell 000, 000")
},
func(id widget.TableCellID, cell fyne.CanvasObject) {
label := cell.(*widget.Label)
switch id.Col {
case 0:
label.SetText(fmt.Sprintf("%d", id.Row+1))
case 1:
label.SetText("A longer cell")
default:
label.SetText(fmt.Sprintf("Cell %d, %d", id.Row+1, id.Col+1))
}
})
t.SetColumnWidth(0, 34)
t.SetColumnWidth(1, 102)
return t
}
func makeTreeTab(_ fyne.Window) fyne.CanvasObject {
data := map[string][]string{
"": {"A"},
"A": {"B", "D", "H", "J", "L", "O", "P", "S", "V"},
"B": {"C"},
"C": {"abc"},
"D": {"E"},
"E": {"F", "G"},
"F": {"adef"},
"G": {"adeg"},
"H": {"I"},
"I": {"ahi"},
"O": {"ao"},
"P": {"Q"},
"Q": {"R"},
"R": {"apqr"},
"S": {"T"},
"T": {"U"},
"U": {"astu"},
"V": {"W"},
"W": {"X"},
"X": {"Y"},
"Y": {"Z"},
"Z": {"avwxyz"},
}
tree := widget.NewTreeWithStrings(data)
tree.OnSelected = func(id string) {
fmt.Println("Tree node selected:", id)
}
tree.OnUnselected = func(id string) {
fmt.Println("Tree node unselected:", id)
}
tree.OpenBranch("A")
tree.OpenBranch("D")
tree.OpenBranch("E")
tree.OpenBranch("L")
tree.OpenBranch("M")
return tree
}

View File

@@ -1,154 +0,0 @@
package tutorials
import (
"fmt"
"image/color"
"strconv"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
// containerScreen loads a tab panel for containers
func containerScreen(_ fyne.Window) fyne.CanvasObject {
content := container.NewBorder(
widget.NewLabelWithStyle("Top", fyne.TextAlignCenter, fyne.TextStyle{}),
widget.NewLabelWithStyle("Bottom", fyne.TextAlignCenter, fyne.TextStyle{}),
widget.NewLabel("Left"),
widget.NewLabel("Right"),
widget.NewLabel("Border Container"))
return container.NewCenter(content)
}
func makeAppTabsTab(_ fyne.Window) fyne.CanvasObject {
tabs := container.NewAppTabs(
container.NewTabItem("Tab 1", widget.NewLabel("Content of tab 1")),
container.NewTabItem("Tab 2 bigger", widget.NewLabel("Content of tab 2")),
container.NewTabItem("Tab 3", widget.NewLabel("Content of tab 3")),
)
for i := 4; i <= 12; i++ {
tabs.Append(container.NewTabItem(fmt.Sprintf("Tab %d", i), widget.NewLabel(fmt.Sprintf("Content of tab %d", i))))
}
locations := makeTabLocationSelect(tabs.SetTabLocation)
return container.NewBorder(locations, nil, nil, nil, tabs)
}
func makeBorderLayout(_ fyne.Window) fyne.CanvasObject {
top := makeCell()
bottom := makeCell()
left := makeCell()
right := makeCell()
middle := widget.NewLabelWithStyle("BorderLayout", fyne.TextAlignCenter, fyne.TextStyle{})
return container.NewBorder(top, bottom, left, right, middle)
}
func makeBoxLayout(_ fyne.Window) fyne.CanvasObject {
top := makeCell()
bottom := makeCell()
middle := widget.NewLabel("BoxLayout")
center := makeCell()
right := makeCell()
col := container.NewVBox(top, middle, bottom)
return container.NewHBox(col, center, right)
}
func makeButtonList(count int) []fyne.CanvasObject {
var items []fyne.CanvasObject
for i := 1; i <= count; i++ {
index := i // capture
items = append(items, widget.NewButton("Button "+strconv.Itoa(index), func() {
fmt.Println("Tapped", index)
}))
}
return items
}
func makeCell() fyne.CanvasObject {
rect := canvas.NewRectangle(&color.NRGBA{128, 128, 128, 255})
rect.SetMinSize(fyne.NewSize(30, 30))
return rect
}
func makeCenterLayout(_ fyne.Window) fyne.CanvasObject {
middle := widget.NewButton("CenterLayout", func() {})
return container.NewCenter(middle)
}
func makeDocTabsTab(_ fyne.Window) fyne.CanvasObject {
tabs := container.NewDocTabs(
container.NewTabItem("Doc 1", widget.NewLabel("Content of document 1")),
container.NewTabItem("Doc 2 bigger", widget.NewLabel("Content of document 2")),
container.NewTabItem("Doc 3", widget.NewLabel("Content of document 3")),
)
i := 3
tabs.CreateTab = func() *container.TabItem {
i++
return container.NewTabItem(fmt.Sprintf("Doc %d", i), widget.NewLabel(fmt.Sprintf("Content of document %d", i)))
}
locations := makeTabLocationSelect(tabs.SetTabLocation)
return container.NewBorder(locations, nil, nil, nil, tabs)
}
func makeGridLayout(_ fyne.Window) fyne.CanvasObject {
box1 := makeCell()
box2 := widget.NewLabel("Grid")
box3 := makeCell()
box4 := makeCell()
return container.NewGridWithColumns(2,
box1, box2, box3, box4)
}
func makeScrollTab(_ fyne.Window) fyne.CanvasObject {
hlist := makeButtonList(20)
vlist := makeButtonList(50)
horiz := container.NewHScroll(container.NewHBox(hlist...))
vert := container.NewVScroll(container.NewVBox(vlist...))
return container.NewAdaptiveGrid(2,
container.NewBorder(horiz, nil, nil, nil, vert),
makeScrollBothTab())
}
func makeScrollBothTab() fyne.CanvasObject {
logo := canvas.NewImageFromResource(theme.FyneLogo())
logo.SetMinSize(fyne.NewSize(800, 800))
scroll := container.NewScroll(logo)
scroll.Resize(fyne.NewSize(400, 400))
return scroll
}
func makeSplitTab(_ fyne.Window) fyne.CanvasObject {
left := widget.NewMultiLineEntry()
left.Wrapping = fyne.TextWrapWord
left.SetText("Long text is looooooooooooooong")
right := container.NewVSplit(
widget.NewLabel("Label"),
widget.NewButton("Button", func() { fmt.Println("button tapped!") }),
)
return container.NewHSplit(container.NewVScroll(left), right)
}
func makeTabLocationSelect(callback func(container.TabLocation)) *widget.Select {
locations := widget.NewSelect([]string{"Top", "Bottom", "Leading", "Trailing"}, func(s string) {
callback(map[string]container.TabLocation{
"Top": container.TabLocationTop,
"Bottom": container.TabLocationBottom,
"Leading": container.TabLocationLeading,
"Trailing": container.TabLocationTrailing,
}[s])
})
locations.SetSelected("Top")
return locations
}

View File

@@ -1,150 +0,0 @@
package tutorials
import (
"fyne.io/fyne/v2"
)
// Tutorial defines the data structure for a tutorial
type Tutorial struct {
Title, Intro string
View func(w fyne.Window) fyne.CanvasObject
}
var (
// Tutorials defines the metadata for each tutorial
Tutorials = map[string]Tutorial{
"welcome": {"Welcome", "", welcomeScreen},
"canvas": {"Canvas",
"See the canvas capabilities.",
canvasScreen,
},
"animations": {"Animations",
"See how to animate components.",
makeAnimationScreen,
},
"icons": {"Theme Icons",
"Browse the embedded icons.",
iconScreen,
},
"containers": {"Containers",
"Containers group other widgets and canvas objects, organising according to their layout.\n" +
"Standard containers are illustrated in this section, but developers can also provide custom " +
"layouts using the fyne.NewContainerWithLayout() constructor.",
containerScreen,
},
"apptabs": {"AppTabs",
"A container to help divide up an application into functional areas.",
makeAppTabsTab,
},
"border": {"Border",
"A container that positions items around a central content.",
makeBorderLayout,
},
"box": {"Box",
"A container arranges items in horizontal or vertical list.",
makeBoxLayout,
},
"center": {"Center",
"A container to that centers child elements.",
makeCenterLayout,
},
"doctabs": {"DocTabs",
"A container to display a single document from a set of many.",
makeDocTabsTab,
},
"grid": {"Grid",
"A container that arranges all items in a grid.",
makeGridLayout,
},
"split": {"Split",
"A split container divides the container in two pieces that the user can resize.",
makeSplitTab,
},
"scroll": {"Scroll",
"A container that provides scrolling for it's content.",
makeScrollTab,
},
"widgets": {"Widgets",
"In this section you can see the features available in the toolkit widget set.\n" +
"Expand the tree on the left to browse the individual tutorial elements.",
widgetScreen,
},
"accordion": {"Accordion",
"Expand or collapse content panels.",
makeAccordionTab,
},
"button": {"Button",
"Simple widget for user tap handling.",
makeButtonTab,
},
"card": {"Card",
"Group content and widgets.",
makeCardTab,
},
"entry": {"Entry",
"Different ways to use the entry widget.",
makeEntryTab,
},
"form": {"Form",
"Gathering input widgets for data submission.",
makeFormTab,
},
"input": {"Input",
"A collection of widgets for user input.",
makeInputTab,
},
"text": {"Text",
"Text handling widgets.",
makeTextTab,
},
"toolbar": {"Toolbar",
"A row of shortcut icons for common tasks.",
makeToolbarTab,
},
"progress": {"Progress",
"Show duration or the need to wait for a task.",
makeProgressTab,
},
"collections": {"Collections",
"Collection widgets provide an efficient way to present lots of content.\n" +
"The List, Table, and Tree provide a cache and re-use mechanism that make it possible to scroll through thousands of elements.\n" +
"Use this for large data sets or for collections that can expand as users scroll.",
collectionScreen,
},
"list": {"List",
"A vertical arrangement of cached elements with the same styling.",
makeListTab,
},
"table": {"Table",
"A two dimensional cached collection of cells.",
makeTableTab,
},
"tree": {"Tree",
"A tree based arrangement of cached elements with the same styling.",
makeTreeTab,
},
"dialogs": {"Dialogs",
"Work with dialogs.",
dialogScreen,
},
"windows": {"Windows",
"Window function demo.",
windowScreen,
},
"binding": {"Data Binding",
"Connecting widgets to a data source.",
bindingScreen},
"advanced": {"Advanced",
"Debug and advanced information.",
advancedScreen,
},
}
// TutorialIndex defines how our tutorials should be laid out in the index tree
TutorialIndex = map[string][]string{
"": {"welcome", "canvas", "animations", "icons", "widgets", "collections", "containers", "dialogs", "windows", "binding", "advanced"},
"collections": {"list", "table", "tree"},
"containers": {"apptabs", "border", "box", "center", "doctabs", "grid", "scroll", "split"},
"widgets": {"accordion", "button", "card", "entry", "form", "input", "progress", "text", "toolbar"},
}
)

View File

@@ -1,185 +0,0 @@
package tutorials
import (
"errors"
"fmt"
"image/color"
"io/ioutil"
"log"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/validation"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
func confirmCallback(response bool) {
fmt.Println("Responded with", response)
}
func colorPicked(c color.Color, w fyne.Window) {
log.Println("Color picked:", c)
rectangle := canvas.NewRectangle(c)
size := 2 * theme.IconInlineSize()
rectangle.SetMinSize(fyne.NewSize(size, size))
dialog.ShowCustom("Color Picked", "Ok", rectangle, w)
}
// dialogScreen loads demos of the dialogs we support
func dialogScreen(win fyne.Window) fyne.CanvasObject {
return container.NewVScroll(container.NewVBox(
widget.NewButton("Info", func() {
dialog.ShowInformation("Information", "You should know this thing...", win)
}),
widget.NewButton("Error", func() {
err := errors.New("a dummy error message")
dialog.ShowError(err, win)
}),
widget.NewButton("Confirm", func() {
cnf := dialog.NewConfirm("Confirmation", "Are you enjoying this demo?", confirmCallback, win)
cnf.SetDismissText("Nah")
cnf.SetConfirmText("Oh Yes!")
cnf.Show()
}),
widget.NewButton("File Open With Filter (.jpg or .png)", func() {
fd := dialog.NewFileOpen(func(reader fyne.URIReadCloser, err error) {
if err != nil {
dialog.ShowError(err, win)
return
}
if reader == nil {
log.Println("Cancelled")
return
}
imageOpened(reader)
}, win)
fd.SetFilter(storage.NewExtensionFileFilter([]string{".png", ".jpg", ".jpeg"}))
fd.Show()
}),
widget.NewButton("File Save", func() {
dialog.ShowFileSave(func(writer fyne.URIWriteCloser, err error) {
if err != nil {
dialog.ShowError(err, win)
return
}
if writer == nil {
log.Println("Cancelled")
return
}
fileSaved(writer, win)
}, win)
}),
widget.NewButton("Folder Open", func() {
dialog.ShowFolderOpen(func(list fyne.ListableURI, err error) {
if err != nil {
dialog.ShowError(err, win)
return
}
if list == nil {
log.Println("Cancelled")
return
}
children, err := list.List()
if err != nil {
dialog.ShowError(err, win)
return
}
out := fmt.Sprintf("Folder %s (%d children):\n%s", list.Name(), len(children), list.String())
dialog.ShowInformation("Folder Open", out, win)
}, win)
}),
widget.NewButton("Color Picker", func() {
picker := dialog.NewColorPicker("Pick a Color", "What is your favorite color?", func(c color.Color) {
colorPicked(c, win)
}, win)
picker.Show()
}),
widget.NewButton("Advanced Color Picker", func() {
picker := dialog.NewColorPicker("Pick a Color", "What is your favorite color?", func(c color.Color) {
colorPicked(c, win)
}, win)
picker.Advanced = true
picker.Show()
}),
widget.NewButton("Form Dialog (Login Form)", func() {
username := widget.NewEntry()
username.Validator = validation.NewRegexp(`^[A-Za-z0-9_-]+$`, "username can only contain letters, numbers, '_', and '-'")
password := widget.NewPasswordEntry()
password.Validator = validation.NewRegexp(`^[A-Za-z0-9_-]+$`, "password can only contain letters, numbers, '_', and '-'")
remember := false
items := []*widget.FormItem{
widget.NewFormItem("Username", username),
widget.NewFormItem("Password", password),
widget.NewFormItem("Remember me", widget.NewCheck("", func(checked bool) {
remember = checked
})),
}
dialog.ShowForm("Login...", "Log In", "Cancel", items, func(b bool) {
if !b {
return
}
var rememberText string
if remember {
rememberText = "and remember this login"
}
log.Println("Please Authenticate", username.Text, password.Text, rememberText)
}, win)
}),
))
}
func imageOpened(f fyne.URIReadCloser) {
if f == nil {
log.Println("Cancelled")
return
}
defer f.Close()
showImage(f)
}
func fileSaved(f fyne.URIWriteCloser, w fyne.Window) {
defer f.Close()
_, err := f.Write([]byte("Written by Fyne demo\n"))
if err != nil {
dialog.ShowError(err, w)
}
err = f.Close()
if err != nil {
dialog.ShowError(err, w)
}
log.Println("Saved to...", f.URI())
}
func loadImage(f fyne.URIReadCloser) *canvas.Image {
data, err := ioutil.ReadAll(f)
if err != nil {
fyne.LogError("Failed to load image data", err)
return nil
}
res := fyne.NewStaticResource(f.URI().Name(), data)
return canvas.NewImageFromResource(res)
}
func showImage(f fyne.URIReadCloser) {
img := loadImage(f)
if img == nil {
return
}
img.FillMode = canvas.ImageFillOriginal
w := fyne.CurrentApp().NewWindow(f.URI().Name())
w.SetContent(container.NewScroll(img))
w.Resize(fyne.NewSize(320, 240))
w.Show()
}

View File

@@ -1,195 +0,0 @@
package tutorials
import (
"image/color"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
type iconInfo struct {
name string
icon fyne.Resource
}
type browser struct {
current int
icons []iconInfo
name *widget.Select
icon *widget.Icon
}
func (b *browser) setIcon(index int) {
if index < 0 || index > len(b.icons)-1 {
return
}
b.current = index
b.name.SetSelected(b.icons[index].name)
b.icon.SetResource(b.icons[index].icon)
}
// iconScreen loads a panel that shows the various icons available in Fyne
func iconScreen(_ fyne.Window) fyne.CanvasObject {
b := &browser{}
b.icons = loadIcons()
prev := widget.NewButtonWithIcon("", theme.NavigateBackIcon(), func() {
b.setIcon(b.current - 1)
})
next := widget.NewButtonWithIcon("", theme.NavigateNextIcon(), func() {
b.setIcon(b.current + 1)
})
b.name = widget.NewSelect(iconList(b.icons), func(name string) {
for i, icon := range b.icons {
if icon.name == name {
if b.current != i {
b.setIcon(i)
}
break
}
}
})
b.name.SetSelected(b.icons[b.current].name)
buttons := container.NewHBox(prev, next)
bar := container.NewBorder(nil, nil, buttons, nil, b.name)
background := canvas.NewRasterWithPixels(checkerPattern)
background.SetMinSize(fyne.NewSize(280, 280))
b.icon = widget.NewIcon(b.icons[b.current].icon)
return fyne.NewContainerWithLayout(layout.NewBorderLayout(
bar, nil, nil, nil), bar, background, b.icon)
}
func checkerPattern(x, y, _, _ int) color.Color {
x /= 20
y /= 20
if x%2 == y%2 {
return theme.BackgroundColor()
}
return theme.ShadowColor()
}
func iconList(icons []iconInfo) []string {
ret := make([]string, len(icons))
for i, icon := range icons {
ret[i] = icon.name
}
return ret
}
func loadIcons() []iconInfo {
return []iconInfo{
{"CancelIcon", theme.CancelIcon()},
{"ConfirmIcon", theme.ConfirmIcon()},
{"DeleteIcon", theme.DeleteIcon()},
{"SearchIcon", theme.SearchIcon()},
{"SearchReplaceIcon", theme.SearchReplaceIcon()},
{"CheckButtonIcon", theme.CheckButtonIcon()},
{"CheckButtonCheckedIcon", theme.CheckButtonCheckedIcon()},
{"RadioButtonIcon", theme.RadioButtonIcon()},
{"RadioButtonCheckedIcon", theme.RadioButtonCheckedIcon()},
{"ColorAchromaticIcon", theme.ColorAchromaticIcon()},
{"ColorChromaticIcon", theme.ColorChromaticIcon()},
{"ColorPaletteIcon", theme.ColorPaletteIcon()},
{"ContentAddIcon", theme.ContentAddIcon()},
{"ContentRemoveIcon", theme.ContentRemoveIcon()},
{"ContentClearIcon", theme.ContentClearIcon()},
{"ContentCutIcon", theme.ContentCutIcon()},
{"ContentCopyIcon", theme.ContentCopyIcon()},
{"ContentPasteIcon", theme.ContentPasteIcon()},
{"ContentRedoIcon", theme.ContentRedoIcon()},
{"ContentUndoIcon", theme.ContentUndoIcon()},
{"InfoIcon", theme.InfoIcon()},
{"ErrorIcon", theme.ErrorIcon()},
{"QuestionIcon", theme.QuestionIcon()},
{"WarningIcon", theme.WarningIcon()},
{"DocumentIcon", theme.DocumentIcon()},
{"DocumentCreateIcon", theme.DocumentCreateIcon()},
{"DocumentPrintIcon", theme.DocumentPrintIcon()},
{"DocumentSaveIcon", theme.DocumentSaveIcon()},
{"FileIcon", theme.FileIcon()},
{"FileApplicationIcon", theme.FileApplicationIcon()},
{"FileAudioIcon", theme.FileAudioIcon()},
{"FileImageIcon", theme.FileImageIcon()},
{"FileTextIcon", theme.FileTextIcon()},
{"FileVideoIcon", theme.FileVideoIcon()},
{"FolderIcon", theme.FolderIcon()},
{"FolderNewIcon", theme.FolderNewIcon()},
{"FolderOpenIcon", theme.FolderOpenIcon()},
{"ComputerIcon", theme.ComputerIcon()},
{"HomeIcon", theme.HomeIcon()},
{"HelpIcon", theme.HelpIcon()},
{"HistoryIcon", theme.HistoryIcon()},
{"SettingsIcon", theme.SettingsIcon()},
{"StorageIcon", theme.StorageIcon()},
{"DownloadIcon", theme.DownloadIcon()},
{"UploadIcon", theme.UploadIcon()},
{"ViewFullScreenIcon", theme.ViewFullScreenIcon()},
{"ViewRestoreIcon", theme.ViewRestoreIcon()},
{"ViewRefreshIcon", theme.ViewRefreshIcon()},
{"VisibilityIcon", theme.VisibilityIcon()},
{"VisibilityOffIcon", theme.VisibilityOffIcon()},
{"ZoomFitIcon", theme.ZoomFitIcon()},
{"ZoomInIcon", theme.ZoomInIcon()},
{"ZoomOutIcon", theme.ZoomOutIcon()},
{"MoreHorizontalIcon", theme.MoreHorizontalIcon()},
{"MoreVerticalIcon", theme.MoreVerticalIcon()},
{"MoveDownIcon", theme.MoveDownIcon()},
{"MoveUpIcon", theme.MoveUpIcon()},
{"NavigateBackIcon", theme.NavigateBackIcon()},
{"NavigateNextIcon", theme.NavigateNextIcon()},
{"Menu", theme.MenuIcon()},
{"MenuExpand", theme.MenuExpandIcon()},
{"MenuDropDown", theme.MenuDropDownIcon()},
{"MenuDropUp", theme.MenuDropUpIcon()},
{"MailAttachmentIcon", theme.MailAttachmentIcon()},
{"MailComposeIcon", theme.MailComposeIcon()},
{"MailForwardIcon", theme.MailForwardIcon()},
{"MailReplyIcon", theme.MailReplyIcon()},
{"MailReplyAllIcon", theme.MailReplyAllIcon()},
{"MailSendIcon", theme.MailSendIcon()},
{"MediaFastForward", theme.MediaFastForwardIcon()},
{"MediaFastRewind", theme.MediaFastRewindIcon()},
{"MediaPause", theme.MediaPauseIcon()},
{"MediaPlay", theme.MediaPlayIcon()},
{"MediaStop", theme.MediaStopIcon()},
{"MediaRecord", theme.MediaRecordIcon()},
{"MediaReplay", theme.MediaReplayIcon()},
{"MediaSkipNext", theme.MediaSkipNextIcon()},
{"MediaSkipPrevious", theme.MediaSkipPreviousIcon()},
{"VolumeDown", theme.VolumeDownIcon()},
{"VolumeMute", theme.VolumeMuteIcon()},
{"VolumeUp", theme.VolumeUpIcon()},
{"AccountIcon", theme.AccountIcon()},
{"LoginIcon", theme.LoginIcon()},
{"LogoutIcon", theme.LogoutIcon()},
{"ListIcon", theme.ListIcon()},
{"GridIcon", theme.GridIcon()},
}
}

View File

@@ -1,72 +0,0 @@
package tutorials
import (
"image/color"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/theme"
)
var (
purple = &color.NRGBA{R: 128, G: 0, B: 128, A: 255}
orange = &color.NRGBA{R: 198, G: 123, B: 0, A: 255}
grey = &color.Gray{Y: 123}
)
// customTheme is a simple demonstration of a bespoke theme loaded by a Fyne app.
type customTheme struct {
}
func (customTheme) Color(c fyne.ThemeColorName, _ fyne.ThemeVariant) color.Color {
switch c {
case theme.ColorNameBackground:
return purple
case theme.ColorNameButton, theme.ColorNameDisabled:
return color.Black
case theme.ColorNamePlaceHolder, theme.ColorNameScrollBar:
return grey
case theme.ColorNamePrimary, theme.ColorNameHover, theme.ColorNameFocus:
return orange
case theme.ColorNameShadow:
return &color.RGBA{R: 0xcc, G: 0xcc, B: 0xcc, A: 0xcc}
default:
return color.White
}
}
func (customTheme) Font(style fyne.TextStyle) fyne.Resource {
return theme.DarkTheme().Font(style)
}
func (customTheme) Icon(n fyne.ThemeIconName) fyne.Resource {
return theme.DefaultTheme().Icon(n)
}
func (customTheme) Size(s fyne.ThemeSizeName) float32 {
switch s {
case theme.SizeNamePadding:
return 8
case theme.SizeNameInlineIcon:
return 20
case theme.SizeNameScrollBar:
return 10
case theme.SizeNameScrollBarSmall:
return 5
case theme.SizeNameText:
return 18
case theme.SizeNameHeadingText:
return 30
case theme.SizeNameSubHeadingText:
return 25
case theme.SizeNameCaptionText:
return 15
case theme.SizeNameInputBorder:
return 1
default:
return 0
}
}
func newCustomTheme() fyne.Theme {
return &customTheme{}
}

View File

@@ -1,42 +0,0 @@
package tutorials
import (
"net/url"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/cmd/fyne_demo/data"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
func parseURL(urlStr string) *url.URL {
link, err := url.Parse(urlStr)
if err != nil {
fyne.LogError("Could not parse URL", err)
}
return link
}
func welcomeScreen(_ fyne.Window) fyne.CanvasObject {
logo := canvas.NewImageFromResource(data.FyneScene)
logo.FillMode = canvas.ImageFillContain
if fyne.CurrentDevice().IsMobile() {
logo.SetMinSize(fyne.NewSize(171, 125))
} else {
logo.SetMinSize(fyne.NewSize(228, 167))
}
return container.NewCenter(container.NewVBox(
widget.NewLabelWithStyle("Welcome to the Fyne toolkit demo app", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
logo,
container.NewHBox(
widget.NewHyperlink("fyne.io", parseURL("https://fyne.io/")),
widget.NewLabel("-"),
widget.NewHyperlink("documentation", parseURL("https://developer.fyne.io/")),
widget.NewLabel("-"),
widget.NewHyperlink("sponsor", parseURL("https://fyne.io/sponsor/")),
),
))
}

View File

@@ -1,420 +0,0 @@
package tutorials
import (
"fmt"
"image/color"
"net/url"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/validation"
"fyne.io/fyne/v2/driver/mobile"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
const (
loremIpsum = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque quis consectetur nisi. Suspendisse id interdum felis.
Sed egestas eget tellus eu pharetra. Praesent pulvinar sed massa id placerat. Etiam sem libero, semper vitae consequat ut, volutpat id mi.
Mauris volutpat pellentesque convallis. Curabitur rutrum venenatis orci nec ornare. Maecenas quis pellentesque neque.
Aliquam consectetur dapibus nulla, id maximus odio ultrices ac. Sed luctus at felis sed faucibus. Cras leo augue, congue in velit ut, mattis rhoncus lectus.
Praesent viverra, mauris ut ullamcorper semper, leo urna auctor lectus, vitae vehicula mi leo quis lorem.
Nullam condimentum, massa at tempor feugiat, metus enim lobortis velit, eget suscipit eros ipsum quis tellus. Aenean fermentum diam vel felis dictum semper.
Duis nisl orci, tincidunt ut leo quis, luctus vehicula diam. Sed velit justo, congue id augue eu, euismod dapibus lacus. Proin sit amet imperdiet sapien.
Mauris erat urna, fermentum et quam rhoncus, fringilla consequat ante. Vivamus consectetur molestie odio, ac rutrum erat finibus a.
Suspendisse id maximus felis. Sed mauris odio, mattis eget mi eu, consequat tempus purus.`
)
var (
progress *widget.ProgressBar
fprogress *widget.ProgressBar
infProgress *widget.ProgressBarInfinite
endProgress chan interface{}
)
func makeAccordionTab(_ fyne.Window) fyne.CanvasObject {
link, err := url.Parse("https://fyne.io/")
if err != nil {
fyne.LogError("Could not parse URL", err)
}
ac := widget.NewAccordion(
widget.NewAccordionItem("A", widget.NewHyperlink("One", link)),
widget.NewAccordionItem("B", widget.NewLabel("Two")),
&widget.AccordionItem{
Title: "C",
Detail: widget.NewLabel("Three"),
},
)
ac.Append(widget.NewAccordionItem("D", &widget.Entry{Text: "Four"}))
return ac
}
func makeButtonTab(_ fyne.Window) fyne.CanvasObject {
disabled := widget.NewButton("Disabled", func() {})
disabled.Disable()
shareItem := fyne.NewMenuItem("Share via", nil)
shareItem.ChildMenu = fyne.NewMenu("",
fyne.NewMenuItem("Twitter", func() { fmt.Println("context menu Share->Twitter") }),
fyne.NewMenuItem("Reddit", func() { fmt.Println("context menu Share->Reddit") }),
)
menuLabel := newContextMenuButton("tap me for pop-up menu with submenus", fyne.NewMenu("",
fyne.NewMenuItem("Copy", func() { fmt.Println("context menu copy") }),
shareItem,
))
return container.NewVBox(
widget.NewButton("Button (text only)", func() { fmt.Println("tapped text button") }),
widget.NewButtonWithIcon("Button (text & leading icon)", theme.ConfirmIcon(), func() { fmt.Println("tapped text & leading icon button") }),
&widget.Button{
Alignment: widget.ButtonAlignLeading,
Text: "Button (leading-aligned, text only)",
OnTapped: func() { fmt.Println("tapped leading-aligned, text only button") },
},
&widget.Button{
Alignment: widget.ButtonAlignTrailing,
IconPlacement: widget.ButtonIconTrailingText,
Text: "Button (trailing-aligned, text & trailing icon)",
Icon: theme.ConfirmIcon(),
OnTapped: func() { fmt.Println("tapped trailing-aligned, text & trailing icon button") },
},
disabled,
layout.NewSpacer(),
layout.NewSpacer(),
menuLabel,
layout.NewSpacer(),
)
}
func makeCardTab(_ fyne.Window) fyne.CanvasObject {
card1 := widget.NewCard("Book a table", "Which time suits?",
widget.NewRadioGroup([]string{"6:30pm", "7:00pm", "7:45pm"}, func(string) {}))
card2 := widget.NewCard("With media", "No content, with image", nil)
card2.Image = canvas.NewImageFromResource(theme.FyneLogo())
card3 := widget.NewCard("Title 3", "Another card", widget.NewLabel("Content"))
return container.NewGridWithColumns(2, container.NewVBox(card1, card3),
container.NewVBox(card2))
}
func makeEntryTab(_ fyne.Window) fyne.CanvasObject {
entry := widget.NewEntry()
entry.SetPlaceHolder("Entry")
entryDisabled := widget.NewEntry()
entryDisabled.SetText("Entry (disabled)")
entryDisabled.Disable()
entryValidated := newNumEntry()
entryValidated.SetPlaceHolder("Must contain a number")
entryMultiLine := widget.NewMultiLineEntry()
entryMultiLine.SetPlaceHolder("MultiLine Entry")
return container.NewVBox(
entry,
entryDisabled,
entryValidated,
entryMultiLine)
}
func makeTextGrid() *widget.TextGrid {
grid := widget.NewTextGridFromString("TextGrid\n\tContent\nZebra")
grid.SetStyleRange(0, 4, 0, 7,
&widget.CustomTextGridStyle{BGColor: &color.NRGBA{R: 64, G: 64, B: 192, A: 128}})
grid.SetRowStyle(1, &widget.CustomTextGridStyle{BGColor: &color.NRGBA{R: 64, G: 192, B: 64, A: 128}})
white := &widget.CustomTextGridStyle{FGColor: color.White, BGColor: color.Black}
black := &widget.CustomTextGridStyle{FGColor: color.Black, BGColor: color.White}
grid.Rows[2].Cells[0].Style = white
grid.Rows[2].Cells[1].Style = black
grid.Rows[2].Cells[2].Style = white
grid.Rows[2].Cells[3].Style = black
grid.Rows[2].Cells[4].Style = white
grid.ShowLineNumbers = true
grid.ShowWhitespace = true
return grid
}
func makeTextTab(_ fyne.Window) fyne.CanvasObject {
label := widget.NewLabel("Label")
link, err := url.Parse("https://fyne.io/")
if err != nil {
fyne.LogError("Could not parse URL", err)
}
hyperlink := widget.NewHyperlink("Hyperlink", link)
entryLoremIpsum := widget.NewMultiLineEntry()
entryLoremIpsum.SetText(loremIpsum)
label.Alignment = fyne.TextAlignLeading
hyperlink.Alignment = fyne.TextAlignLeading
label.Wrapping = fyne.TextWrapWord
hyperlink.Wrapping = fyne.TextWrapWord
entryLoremIpsum.Wrapping = fyne.TextWrapWord
rich := widget.NewRichTextFromMarkdown(`
# RichText Heading
## A Sub Heading
---
* Item1 in _three_ segments
* Item2
* Item3
Normal **Bold** *Italic* [Link](https://fyne.io/) and some ` + "`Code`" + `.
This styled row should also wrap as expected, but only *when required*.
> An interesting quote here, most likely sharing some very interesting wisdom.`)
rich.Scroll = container.ScrollBoth
radioAlign := widget.NewRadioGroup([]string{"Text Alignment Leading", "Text Alignment Center", "Text Alignment Trailing"}, func(s string) {
var align fyne.TextAlign
switch s {
case "Text Alignment Leading":
align = fyne.TextAlignLeading
case "Text Alignment Center":
align = fyne.TextAlignCenter
case "Text Alignment Trailing":
align = fyne.TextAlignTrailing
}
label.Alignment = align
hyperlink.Alignment = align
for i := range rich.Segments {
if seg, ok := rich.Segments[i].(*widget.TextSegment); ok {
seg.Style.Alignment = align
}
if seg, ok := rich.Segments[i].(*widget.HyperlinkSegment); ok {
seg.Alignment = align
}
}
label.Refresh()
hyperlink.Refresh()
rich.Refresh()
})
radioAlign.SetSelected("Text Alignment Leading")
radioWrap := widget.NewRadioGroup([]string{"Text Wrapping Off", "Text Wrapping Truncate", "Text Wrapping Break", "Text Wrapping Word"}, func(s string) {
var wrap fyne.TextWrap
switch s {
case "Text Wrapping Off":
wrap = fyne.TextWrapOff
case "Text Wrapping Truncate":
wrap = fyne.TextTruncate
case "Text Wrapping Break":
wrap = fyne.TextWrapBreak
case "Text Wrapping Word":
wrap = fyne.TextWrapWord
}
label.Wrapping = wrap
hyperlink.Wrapping = wrap
entryLoremIpsum.Wrapping = wrap
rich.Wrapping = wrap
label.Refresh()
hyperlink.Refresh()
entryLoremIpsum.Refresh()
rich.Refresh()
})
radioWrap.SetSelected("Text Wrapping Word")
fixed := container.NewVBox(
container.NewHBox(
radioAlign,
layout.NewSpacer(),
radioWrap,
),
label,
hyperlink,
)
grid := makeTextGrid()
return container.NewBorder(fixed, grid, nil, nil,
container.NewGridWithRows(2, rich, entryLoremIpsum))
}
func makeInputTab(_ fyne.Window) fyne.CanvasObject {
selectEntry := widget.NewSelectEntry([]string{"Option A", "Option B", "Option C"})
selectEntry.PlaceHolder = "Type or select"
disabledCheck := widget.NewCheck("Disabled check", func(bool) {})
disabledCheck.Disable()
checkGroup := widget.NewCheckGroup([]string{"CheckGroup Item 1", "CheckGroup Item 2AAAAAAAAAAAAAA", "CheckGroup Item 3"}, func(s []string) { fmt.Println("selected", s) })
checkGroup.Horizontal = true
radio := widget.NewRadioGroup([]string{"Radio Item 1", "Radio Item 2"}, func(s string) { fmt.Println("selected", s) })
radio.Horizontal = true
disabledRadio := widget.NewRadioGroup([]string{"Disabled radio"}, func(string) {})
disabledRadio.Disable()
return container.NewVBox(
widget.NewSelect([]string{"Option 1", "Option 2", "Option 3"}, func(s string) { fmt.Println("selected", s) }),
selectEntry,
widget.NewCheck("Check", func(on bool) { fmt.Println("checked", on) }),
disabledCheck,
checkGroup,
radio,
disabledRadio,
widget.NewSlider(0, 100),
)
}
func makeProgressTab(_ fyne.Window) fyne.CanvasObject {
stopProgress()
progress = widget.NewProgressBar()
fprogress = widget.NewProgressBar()
fprogress.TextFormatter = func() string {
return fmt.Sprintf("%.2f out of %.2f", fprogress.Value, fprogress.Max)
}
infProgress = widget.NewProgressBarInfinite()
endProgress = make(chan interface{}, 1)
startProgress()
return container.NewVBox(
widget.NewLabel("Percent"), progress,
widget.NewLabel("Formatted"), fprogress,
widget.NewLabel("Infinite"), infProgress)
}
func makeFormTab(_ fyne.Window) fyne.CanvasObject {
name := widget.NewEntry()
name.SetPlaceHolder("John Smith")
email := widget.NewEntry()
email.SetPlaceHolder("test@example.com")
email.Validator = validation.NewRegexp(`\w{1,}@\w{1,}\.\w{1,4}`, "not a valid email")
password := widget.NewPasswordEntry()
password.SetPlaceHolder("Password")
disabled := widget.NewRadioGroup([]string{"Option 1", "Option 2"}, func(string) {})
disabled.Horizontal = true
disabled.Disable()
largeText := widget.NewMultiLineEntry()
form := &widget.Form{
Items: []*widget.FormItem{
{Text: "Name", Widget: name, HintText: "Your full name"},
{Text: "Email", Widget: email, HintText: "A valid email address"},
},
OnCancel: func() {
fmt.Println("Cancelled")
},
OnSubmit: func() {
fmt.Println("Form submitted")
fyne.CurrentApp().SendNotification(&fyne.Notification{
Title: "Form for: " + name.Text,
Content: largeText.Text,
})
},
}
form.Append("Password", password)
form.Append("Disabled", disabled)
form.Append("Message", largeText)
return form
}
func makeToolbarTab(_ fyne.Window) fyne.CanvasObject {
t := widget.NewToolbar(widget.NewToolbarAction(theme.MailComposeIcon(), func() { fmt.Println("New") }),
widget.NewToolbarSeparator(),
widget.NewToolbarSpacer(),
widget.NewToolbarAction(theme.ContentCutIcon(), func() { fmt.Println("Cut") }),
widget.NewToolbarAction(theme.ContentCopyIcon(), func() { fmt.Println("Copy") }),
widget.NewToolbarAction(theme.ContentPasteIcon(), func() { fmt.Println("Paste") }),
)
return container.NewBorder(t, nil, nil, nil)
}
func startProgress() {
progress.SetValue(0)
fprogress.SetValue(0)
select { // ignore stale end message
case <-endProgress:
default:
}
go func() {
end := endProgress
num := 0.0
for num < 1.0 {
time.Sleep(16 * time.Millisecond)
select {
case <-end:
return
default:
}
progress.SetValue(num)
fprogress.SetValue(num)
num += 0.002
}
progress.SetValue(1)
fprogress.SetValue(1)
// TODO make sure this resets when we hide etc...
stopProgress()
}()
infProgress.Start()
}
func stopProgress() {
if !infProgress.Running() {
return
}
infProgress.Stop()
endProgress <- struct{}{}
}
// widgetScreen shows a panel containing widget demos
func widgetScreen(_ fyne.Window) fyne.CanvasObject {
content := container.NewVBox(
widget.NewLabel("Labels"),
widget.NewButtonWithIcon("Icons", theme.HomeIcon(), func() {}),
widget.NewSlider(0, 1))
return container.NewCenter(content)
}
type contextMenuButton struct {
widget.Button
menu *fyne.Menu
}
func (b *contextMenuButton) Tapped(e *fyne.PointEvent) {
widget.ShowPopUpMenuAtPosition(b.menu, fyne.CurrentApp().Driver().CanvasForObject(b), e.AbsolutePosition)
}
func newContextMenuButton(label string, menu *fyne.Menu) *contextMenuButton {
b := &contextMenuButton{menu: menu}
b.Text = label
b.ExtendBaseWidget(b)
return b
}
type numEntry struct {
widget.Entry
}
func (n *numEntry) Keyboard() mobile.KeyboardType {
return mobile.NumberKeyboard
}
func newNumEntry() *numEntry {
e := &numEntry{}
e.ExtendBaseWidget(e)
e.Validator = validation.NewRegexp(`\d`, "Must contain a number")
return e
}

View File

@@ -1,71 +0,0 @@
package tutorials
import (
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget"
)
func windowScreen(_ fyne.Window) fyne.CanvasObject {
windowGroup := container.NewVBox(
widget.NewButton("New window", func() {
w := fyne.CurrentApp().NewWindow("Hello")
w.SetContent(widget.NewLabel("Hello World!"))
w.Show()
}),
widget.NewButton("Fixed size window", func() {
w := fyne.CurrentApp().NewWindow("Fixed")
w.SetContent(fyne.NewContainerWithLayout(layout.NewCenterLayout(), widget.NewLabel("Hello World!")))
w.Resize(fyne.NewSize(240, 180))
w.SetFixedSize(true)
w.Show()
}),
widget.NewButton("Toggle between fixed/not fixed window size", func() {
w := fyne.CurrentApp().NewWindow("Toggle fixed size")
w.SetContent(fyne.NewContainerWithLayout(layout.NewCenterLayout(), widget.NewCheck("Fixed size", func(toggle bool) {
if toggle {
w.Resize(fyne.NewSize(240, 180))
}
w.SetFixedSize(toggle)
})))
w.Show()
}),
widget.NewButton("Centered window", func() {
w := fyne.CurrentApp().NewWindow("Central")
w.SetContent(fyne.NewContainerWithLayout(layout.NewCenterLayout(), widget.NewLabel("Hello World!")))
w.CenterOnScreen()
w.Show()
}))
drv := fyne.CurrentApp().Driver()
if drv, ok := drv.(desktop.Driver); ok {
windowGroup.Objects = append(windowGroup.Objects,
widget.NewButton("Splash Window (only use on start)", func() {
w := drv.CreateSplashWindow()
w.SetContent(widget.NewLabelWithStyle("Hello World!\n\nMake a splash!",
fyne.TextAlignCenter, fyne.TextStyle{Bold: true}))
w.Show()
go func() {
time.Sleep(time.Second * 3)
w.Close()
}()
}))
}
otherGroup := widget.NewCard("Other", "",
widget.NewButton("Notification", func() {
fyne.CurrentApp().SendNotification(&fyne.Notification{
Title: "Fyne Demo",
Content: "Testing notifications...",
})
}))
return container.NewVBox(widget.NewCard("Windows", "", windowGroup), otherGroup)
}

View File

@@ -1,29 +0,0 @@
package main
import (
"AynaLivePlayer/config"
"AynaLivePlayer/controller"
"AynaLivePlayer/gui"
"AynaLivePlayer/logger"
"AynaLivePlayer/plugin/diange"
"AynaLivePlayer/plugin/qiege"
"AynaLivePlayer/plugin/textinfo"
"AynaLivePlayer/plugin/webinfo"
"AynaLivePlayer/plugin/wylogin"
)
var plugins = []controller.Plugin{diange.NewDiange(), qiege.NewQiege(), textinfo.NewTextInfo(), webinfo.NewWebInfo(),
wylogin.NewWYLogin()}
func main() {
logger.Logger.Info("================Program Start================")
logger.Logger.Infof("================Current Version: %s================", config.Version)
controller.Initialize()
controller.LoadPlugins(plugins...)
gui.Initialize()
gui.MainWindow.ShowAndRun()
controller.ClosePlugins(plugins...)
controller.Destroy()
_ = config.SaveToConfigFile(config.ConfigPath)
logger.Logger.Info("================Program End================")
}

88
app/main.go Normal file
View File

@@ -0,0 +1,88 @@
package main
import (
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/gui"
"AynaLivePlayer/internal"
"AynaLivePlayer/pkg/config"
"AynaLivePlayer/pkg/eventbus"
"AynaLivePlayer/pkg/i18n"
"AynaLivePlayer/pkg/logger"
loggerRepo "AynaLivePlayer/pkg/logger/repository"
"flag"
"os"
"os/signal"
"time"
)
var dev = flag.Bool("dev", false, "dev")
var headless = flag.Bool("headless", false, "headless")
type _LogConfig struct {
config.BaseConfig
Path string
Level logger.LogLevel
RedirectStderr bool
MaxSize int64
}
func (c *_LogConfig) Name() string {
return "Log"
}
var Log = &_LogConfig{
Path: "./log.txt",
Level: logger.LogLevelInfo,
RedirectStderr: false, // this should be true if it is in production mode.
MaxSize: 5,
}
func setupGlobal() {
//global.EventManager = event.NewManger(128, 16)
global.EventBus = eventbus.New()
global.Logger = loggerRepo.NewZapColoredLogger(Log.Path, !*dev)
global.Logger.SetLogLevel(Log.Level)
}
func main() {
flag.Parse()
config.LoadFromFile(config.ConfigPath)
config.LoadConfig(Log)
i18n.LoadLanguage(config.General.Language)
setupGlobal()
global.Logger.Info("================Program Start================")
global.Logger.Infof("================Current Version: %s================", model.Version(config.Version))
internal.Initialize()
go func() {
// temporary fix for gui not render correctly.
// wait until gui rendered then start event dispatching
time.Sleep(1 * time.Second)
//global.EventManager.Start()
_ = global.EventBus.Start()
}()
if *headless || config.Experimental.Headless {
quit := make(chan os.Signal)
signal.Notify(quit, os.Interrupt)
<-quit
} else {
gui.Initialize()
gui.MainWindow.ShowAndRun()
}
global.Logger.Info("closing internal server")
internal.Stop()
global.Logger.Infof("closing event manager")
//global.EventManager.Stop()
_ = global.EventBus.Stop()
if *dev {
global.Logger.Infof("saving translation")
i18n.SaveTranslation()
}
err := config.SaveToConfigFile(config.ConfigPath)
if err != nil {
global.Logger.Errorf("save config failed: %v", err)
} else {
global.Logger.Infof("save config success")
}
global.Logger.Info("================Program End================")
}

32
assets/config/diange.json Normal file
View File

@@ -0,0 +1,32 @@
{
"bilibili-video": {
"enable": true,
"command": "点b歌",
"priority": 3
},
"kugou": {
"enable": true,
"command": "点kg歌",
"priority": 5
},
"kugou-instr": {
"enable": true,
"command": "点伴奏",
"priority": 6
},
"kuwo": {
"enable": true,
"command": "点k歌",
"priority": 2
},
"local": {
"enable": true,
"command": "点local",
"priority": 4
},
"netease": {
"enable": true,
"command": "点w歌",
"priority": 1
}
}

View File

@@ -0,0 +1,12 @@
[
{
"live_room": {
"provider": "biliweb",
"room": "3819533"
},
"config": {
"auto_connect": false
},
"title": "web 3819533"
}
]

2230
assets/config/playlists.json Normal file

File diff suppressed because one or more lines are too long

View File

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

BIN
assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

BIN
assets/icon2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

BIN
assets/msyh0.ttf Normal file

Binary file not shown.

BIN
assets/msyhbd.ttc Normal file

Binary file not shown.

BIN
assets/msyhbd0.ttf Normal file

Binary file not shown.

View File

View File

@@ -0,0 +1,68 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#First released as C++ program by Hiroyuki Tsutsumi as part of the free software suite “Beer”
#I thought porting it to Python could be both a challenge and useful
from sys import argv, exit, getsizeof
from struct import pack_into, unpack_from
def ceil4(n):
"""returns the next integer which is a multiple of 4"""
return (n + 3) & ~3
if len(argv)!=2:
print("Usage: %s FontCollection.ttc" % argv)
exit(2)
filename = argv[1]
in_file = open(filename, "rb")
buf = in_file.read()
in_file.close()
if filename.lower().endswith(".ttc"):
filename = filename[:-4]
if buf[:4] != b"ttcf":
out_filename = "%s.ttf" % filename
out_file = open(out_filename, "wb")
out_file.write(buf)
#end, so we dont have to close the files or call exit() here
else:
ttf_count = unpack_from("!L", buf, 0x08)[0]
print("Anzahl enthaltener TTF-Dateien: %s" % ttf_count)
ttf_offset_array = unpack_from("!"+ttf_count*"L", buf, 0x0C)
for i in range(ttf_count):
print("Extrahiere TTF #%s:" % (i+1))
table_header_offset = ttf_offset_array[i]
print("\tHeader beginnt bei Byte %s" % table_header_offset)
table_count = unpack_from("!H", buf, table_header_offset+0x04)[0]
header_length = 0x0C + table_count * 0x10
print("\tHeaderlänge: %s Byte" % header_length)
table_length = 0
for j in range(table_count):
length = unpack_from("!L", buf, table_header_offset+0x0C+0x0C+j*0x10)[0]
table_length += ceil4(length)
total_length = header_length + table_length
new_buf = bytearray(total_length)
header = unpack_from(header_length*"c", buf, table_header_offset)
pack_into(header_length*"c", new_buf, 0, *header)
current_offset = header_length
for j in range(table_count):
offset = unpack_from("!L", buf, table_header_offset+0x0C+0x08+j*0x10)[0]
length = unpack_from("!L", buf, table_header_offset+0x0C+0x0C+j*0x10)[0]
pack_into("!L", new_buf, 0x0C+0x08+j*0x10, current_offset)
current_table = unpack_from(length*"c", buf, offset)
pack_into(length*"c", new_buf, current_offset, *current_table)
#table_checksum = sum(unpack_from("!"+("L"*length), new_buf, current_offset))
#pack_into("!L", new_buf, 0x0C+0x04+j*0x10, table_checksum)
current_offset += ceil4(length)
out_file = open("%s%d.ttf"%(filename, i), "wb")
out_file.write(new_buf)

View File

@@ -0,0 +1 @@
start AynaLivePlayer.exe --headless

View File

@@ -0,0 +1 @@
taskkill /F /IM AynaLivePlayer.exe

View File

@@ -0,0 +1 @@
taskkill /IM "AynaLivePlayer.exe" /F

View File

@@ -0,0 +1 @@
netsh winsock reset

View File

@@ -4,10 +4,26 @@
"zh-CN"
],
"Messages": {
"default web protocol. enter room id to connect.": {
"en": "default web protocol. enter room id to connect.",
"zh-CN": "网页弹幕协议,请输入房间号"
},
"gui.config.basic.audio_device": {
"en": "Audio Device",
"zh-CN": "音频输出设备"
},
"gui.config.basic.auto_check_update": {
"en": "Check update at startup",
"zh-CN": "自动检查更新"
},
"gui.config.basic.auto_check_update.prompt": {
"en": "Yes",
"zh-CN": "确认"
},
"gui.config.basic.check_update": {
"en": "Check Update",
"zh-CN": "检查更新"
},
"gui.config.basic.description": {
"en": "Basic Configuration",
"zh-CN": "基础设置"
@@ -24,11 +40,11 @@
"en": "User Playlist",
"zh-CN": "用户歌单"
},
"gui.config.basic.skip_playlist": {
"en": "Skip Media From System Playlist",
"zh-CN": "跳过闲置歌单"
"gui.config.basic.skip_when_error": {
"en": "Skip this Media On Error",
"zh-CN": "跳过获取失败的歌曲"
},
"gui.config.basic.skip_playlist.prompt": {
"gui.config.basic.skip_when_error.prompt": {
"en": "Skip",
"zh-CN": "跳过"
},
@@ -36,6 +52,14 @@
"en": "Basic",
"zh-CN": "基础设置"
},
"gui.config.basic.use_system_playlist": {
"en": "Play system playlist when no music",
"zh-CN": "是否播放闲置歌单(实验性)"
},
"gui.config.basic.use_system_playlist.prompt": {
"en": "Yes",
"zh-CN": "是"
},
"gui.history.artist": {
"en": "Artist",
"zh-CN": "歌手"
@@ -52,10 +76,18 @@
"en": "User",
"zh-CN": "用户"
},
"gui.lyric.title": {
"en": "Lyric",
"zh-CN": "歌词"
},
"gui.player.button.lrc": {
"en": "lrc",
"zh-CN": "歌词"
},
"gui.player.button.player": {
"en": "player",
"zh-CN": "播放器"
},
"gui.player.playlist.artist": {
"en": "Artist",
"zh-CN": "歌手"
@@ -128,6 +160,34 @@
"en": "Current: None",
"zh-CN": "当前为: 无"
},
"gui.room.add.cancel": {
"en": "Cancel",
"zh-CN": "取消"
},
"gui.room.add.client_name": {
"en": "Client Name",
"zh-CN": "直播平台"
},
"gui.room.add.confirm": {
"en": "Confirm",
"zh-CN": "确定"
},
"gui.room.add.id_url": {
"en": "Room ID",
"zh-CN": "房间号"
},
"gui.room.add.name": {
"en": "Display Name",
"zh-CN": "显示名"
},
"gui.room.add.prompt": {
"en": "enter room id",
"zh-CN": "填入房间号"
},
"gui.room.add.title": {
"en": "Add Room",
"zh-CN": "添加房间"
},
"gui.room.btn.connect": {
"en": "Connect",
"zh-CN": "连接"
@@ -136,6 +196,18 @@
"en": "Disconnect",
"zh-CN": "断开"
},
"gui.room.button.add": {
"en": "Add",
"zh-CN": "新增"
},
"gui.room.button.remove": {
"en": "Remove",
"zh-CN": "删除"
},
"gui.room.check.autoconnect": {
"en": "Auto Connection",
"zh-CN": "自动连接"
},
"gui.room.id": {
"en": "Room ID: ",
"zh-CN": "房间号: "
@@ -208,22 +280,74 @@
"en": "Search",
"zh-CN": "搜索"
},
"gui.tray.btn.show": {
"en": "Show",
"zh-CN": "打开"
},
"gui.update.already_latest_version": {
"en": "no update available",
"zh-CN": "没有可用更新"
},
"gui.update.new_version": {
"en": "New Version Available",
"zh-CN": "有新版本可用"
},
"open bilibili live protocol. enter client key to connect.": {
"en": "open bilibili live protocol. enter client key to connect.",
"zh-CN": "新版b站协议输入身份码"
},
"plugin.diange.admin": {
"en": "Admin",
"zh-CN": "管理员"
},
"plugin.diange.blacklist.btn.add": {
"en": "Add",
"zh-CN": "添加"
},
"plugin.diange.blacklist.description": {
"en": "Blacklist Configuration",
"zh-CN": "点歌黑名单设置"
},
"plugin.diange.blacklist.input.placeholder": {
"en": "enter word",
"zh-CN": "输入黑名单词"
},
"plugin.diange.blacklist.option.contains": {
"en": "Contains",
"zh-CN": "包含"
},
"plugin.diange.blacklist.option.exact": {
"en": "Exact Match",
"zh-CN": "相等"
},
"plugin.diange.blacklist.title": {
"en": "Blacklist",
"zh-CN": "点歌黑名单"
},
"plugin.diange.cooldown": {
"en": "Cooldown",
"zh-CN": "点歌冷却"
},
"plugin.diange.custom_cmd": {
"en": "Custom Command (Default one still works)",
"zh-CN": "自定义命令 (默认的依然可用)"
"en": "Custom Command",
"zh-CN": "自定义命令"
},
"plugin.diange.description": {
"en": "Basic Diange Configuration",
"zh-CN": "点歌基本设置"
},
"plugin.diange.medal.level": {
"en": "Level",
"zh-CN": "等级"
},
"plugin.diange.medal.name": {
"en": "Name",
"zh-CN": "牌子名"
},
"plugin.diange.medal.perm": {
"en": "Medal Permission",
"zh-CN": "牌子点歌权限"
},
"plugin.diange.permission": {
"en": "Permission",
"zh-CN": "点歌权限"
@@ -236,6 +360,26 @@
"en": "Max Queue",
"zh-CN": "最大点歌数"
},
"plugin.diange.skip_playlist": {
"en": "Skip Media From System Playlist",
"zh-CN": "跳过闲置歌单"
},
"plugin.diange.skip_playlist.prompt": {
"en": "Skip",
"zh-CN": "跳过"
},
"plugin.diange.source.command": {
"en": "command",
"zh-CN": "命令"
},
"plugin.diange.source.enable": {
"en": "enable",
"zh-CN": "启用"
},
"plugin.diange.source.priority": {
"en": "priority",
"zh-CN": "优先级"
},
"plugin.diange.source_cmd": {
"en": "Source Command",
"zh-CN": "来源点歌命令"
@@ -248,45 +392,41 @@
"en": "User",
"zh-CN": "普通用户"
},
"plugin.neteaselogin.current_user": {
"en": "Current User:",
"zh-CN": "当前用户:"
"plugin.diange.user_max": {
"en": "User Maximum queue",
"zh-CN": "单个用户最大点歌数"
},
"plugin.neteaselogin.current_user.notlogin": {
"en": "Not Login",
"zh-CN": "未登录"
"plugin.maxduration.description": {
"en": "Set the maximum duration of a song",
"zh-CN": "设置歌曲最长能播多久"
},
"plugin.neteaselogin.description": {
"en": "Netease User Login",
"zh-CN": "网易云登录"
"plugin.maxduration.enable": {
"en": "Enable",
"zh-CN": "开启"
},
"plugin.neteaselogin.logout": {
"en": "Logout",
"zh-CN": "登出"
"plugin.maxduration.maxduration": {
"en": "Max Duration (seconds)",
"zh-CN": "最大时长 (秒)"
},
"plugin.neteaselogin.qr.finish": {
"en": "Finish Scan",
"zh-CN": "完成扫描后按我"
"plugin.maxduration.skiponplay": {
"en": "Skip on play",
"zh-CN": "播放时跳过"
},
"plugin.neteaselogin.qr.new": {
"en": "Get a new qr code",
"zh-CN": "获取新二维码"
"plugin.maxduration.skiponreach": {
"en": "Skip when reach max duration",
"zh-CN": "播放到最大时长时跳过"
},
"plugin.neteaselogin.refresh": {
"en": "Refresh",
"zh-CN": "刷新状态"
},
"plugin.neteaselogin.title": {
"en": "Netease Login",
"zh-CN": "网易云登录"
"plugin.maxduration.title": {
"en": "Audio Duration Control",
"zh-CN": "歌曲时长控制"
},
"plugin.qiege.admin": {
"en": "Admin",
"zh-CN": "管理员"
},
"plugin.qiege.custom_cmd": {
"en": "Custom Command (Default one still works)",
"zh-CN": "自定义命令 (默认的依然可用)"
"en": "Custom Command",
"zh-CN": "自定义命令"
},
"plugin.qiege.description": {
"en": "Basic Qiege configuration",
@@ -308,6 +448,42 @@
"en": "User",
"zh-CN": "切自己"
},
"plugin.sourcelogin.current_user": {
"en": "Current Status:",
"zh-CN": "当前状态:"
},
"plugin.sourcelogin.current_user.loggedin": {
"en": "Logged In",
"zh-CN": "已登录"
},
"plugin.sourcelogin.current_user.notlogin": {
"en": "Not Login",
"zh-CN": "未登录"
},
"plugin.sourcelogin.description": {
"en": "Netease User Login",
"zh-CN": "来源登录"
},
"plugin.sourcelogin.logout": {
"en": "Logout",
"zh-CN": "登出"
},
"plugin.sourcelogin.qr.finish": {
"en": "Finish Scan",
"zh-CN": "完成扫描后按我"
},
"plugin.sourcelogin.qr.new": {
"en": "Get a new qr code",
"zh-CN": "获取新二维码"
},
"plugin.sourcelogin.refresh": {
"en": "Refresh",
"zh-CN": "刷新状态"
},
"plugin.sourcelogin.title": {
"en": "Netease Login",
"zh-CN": "来源登录"
},
"plugin.textinfo.checkbox": {
"en": "Enable",
"zh-CN": "开启"
@@ -324,49 +500,93 @@
"en": "Text Output",
"zh-CN": "文本输出"
},
"plugin.webinfo.description": {
"en": "Web output configuration",
"zh-CN": "web输出设置"
"plugin.wshub.autostart": {
"en": "Auto start",
"zh-CN": "自动启用"
},
"plugin.webinfo.port": {
"plugin.wshub.description": {
"en": "Websocket Hub Configuration",
"zh-CN": "Websocket服务器设置"
},
"plugin.wshub.local_host_only": {
"en": "only allow local host connection",
"zh-CN": "只允许本地连接"
},
"plugin.wshub.port": {
"en": "Port",
"zh-CN": "服务器端口"
},
"plugin.webinfo.server_control": {
"plugin.wshub.server_control": {
"en": "Control",
"zh-CN": "操作"
},
"plugin.webinfo.server_control.restart": {
"plugin.wshub.server_control.restart": {
"en": "Restart",
"zh-CN": "重启"
},
"plugin.webinfo.server_control.start": {
"plugin.wshub.server_control.start": {
"en": "Start",
"zh-CN": "启动"
},
"plugin.webinfo.server_control.stop": {
"plugin.wshub.server_control.stop": {
"en": "Stop",
"zh-CN": "停止"
},
"plugin.webinfo.server_status": {
"plugin.wshub.server_link": {
"en": "Websocket Server Link",
"zh-CN": "Websocket服务器链接"
},
"plugin.wshub.server_status": {
"en": "Server Status",
"zh-CN": "服务器状态"
},
"plugin.webinfo.server_status.running": {
"plugin.wshub.server_status.running": {
"en": "Running",
"zh-CN": "运行中"
},
"plugin.webinfo.server_status.stopped": {
"plugin.wshub.server_status.stopped": {
"en": "Stopped",
"zh-CN": "已停止"
},
"plugin.webinfo.title": {
"en": "Web Output",
"zh-CN": "Web输出"
"plugin.wshub.title": {
"en": "Websocket Hub",
"zh-CN": "Websocket服务器"
},
"plugin.webinfo.server_preview": {
"en": "Server Preview",
"zh-CN":"效果预览"
"plugin.wshub.webinfo_text": {
"en": "Obs browser output",
"zh-CN": "OBS网页输出: "
},
"plugin.yinliang.title": {
"en": "Volume Control",
"zh-CN": "音量控制"
},
"plugin.yinliang.description": {
"en": "Control volume via danmaku",
"zh-CN": "通过弹幕控制音量"
},
"plugin.yinliang.admin_permission": {
"en": "Admin only",
"zh-CN": "仅房管可操作"
},
"plugin.yinliang.enabled": {
"en": "Enabled volume control",
"zh-CN": "启用弹幕音量控制"
},
"plugin.yinliang.volume_up_cmd": {
"en": "Volume increase command",
"zh-CN": "音量增加命令"
},
"plugin.yinliang.volume_down_cmd": {
"en": "Volume decrease command",
"zh-CN": "音量减少命令"
},
"plugin.yinliang.volume_step": {
"en": "Adjustment step (%)",
"zh-CN": "每次音量调整 (%)"
},
"plugin.yinliang.max_volume": {
"en": "Maximum volume (%)",
"zh-CN": "最大音量限制 (%)"
}
}
}

View File

@@ -1,13 +0,0 @@
package config
type _GeneralConfig struct {
Language string
}
func (c *_GeneralConfig) Name() string {
return "General"
}
var General = &_GeneralConfig{
Language: "en",
}

View File

@@ -1,11 +0,0 @@
package config
type _LiveRoomConfig struct {
History []string
}
func (c *_LiveRoomConfig) Name() string {
return "LiveRoom"
}
var LiveRoom = &_LiveRoomConfig{History: []string{"9076804", "3819533"}}

View File

@@ -1,19 +0,0 @@
package config
import "github.com/sirupsen/logrus"
type _LogConfig struct {
Path string
Level logrus.Level
RedirectStderr bool
}
func (c *_LogConfig) Name() string {
return "Log"
}
var Log = &_LogConfig{
Path: "./log.txt",
Level: logrus.InfoLevel,
RedirectStderr: false, // this should be true if it is in production mode.
}

View File

@@ -1,25 +0,0 @@
package config
type _PlayerConfig struct {
Playlists []string
PlaylistsProvider []string
PlaylistIndex int
PlaylistRandom bool
AudioDevice string
Volume float64
SkipPlaylist bool
}
func (c *_PlayerConfig) Name() string {
return "Player"
}
var Player = &_PlayerConfig{
Playlists: []string{"2382819181", "4987059624", "list1"},
PlaylistsProvider: []string{"netease", "netease", "local"},
PlaylistIndex: 0,
PlaylistRandom: true,
AudioDevice: "auto",
Volume: 100,
SkipPlaylist: false,
}

View File

@@ -1,15 +0,0 @@
package config
type _ProviderConfig struct {
Priority []string
LocalDir string
}
func (c *_ProviderConfig) Name() string {
return "Provider"
}
var Provider = &_ProviderConfig{
Priority: []string{"netease", "kuwo", "bilibili", "local", "bilibili-video"},
LocalDir: "./music",
}

View File

@@ -1,15 +0,0 @@
package config
import (
"fmt"
"testing"
)
func TestCreate(t *testing.T) {
fmt.Println(SaveToConfigFile(ConfigPath))
}
func TestLoad(t *testing.T) {
fmt.Println(Log.Path)
fmt.Println(Player.Playlists)
}

View File

@@ -1,31 +0,0 @@
package controller
import (
"AynaLivePlayer/event"
"AynaLivePlayer/liveclient"
"strings"
)
var Commands []DanmuCommandExecutor
type DanmuCommandExecutor interface {
Match(command string) bool
Execute(command string, args []string, danmu *liveclient.DanmuMessage)
}
func AddCommand(executors ...DanmuCommandExecutor) {
Commands = append(Commands, executors...)
}
func danmuCommandHandler(event *event.Event) {
danmu := event.Data.(*liveclient.DanmuMessage)
args := strings.Split(danmu.Message, " ")
if len(args[0]) == 0 {
return
}
for _, cmd := range Commands {
if cmd.Match(args[0]) {
cmd.Execute(args[0], args[1:], danmu)
}
}
}

View File

@@ -1,122 +0,0 @@
package controller
import (
"AynaLivePlayer/config"
"AynaLivePlayer/event"
"AynaLivePlayer/liveclient"
"AynaLivePlayer/logger"
"AynaLivePlayer/player"
"AynaLivePlayer/provider"
"AynaLivePlayer/util"
"fmt"
"github.com/sirupsen/logrus"
"strconv"
)
const MODULE_CONTROLLER = "Controller"
func l() *logrus.Entry {
return logger.Logger.WithField("Module", MODULE_CONTROLLER)
}
func SetDanmuClient(roomId string) {
ResetDanmuClient()
l().Infof("setting live client for %s", roomId)
room, err := strconv.Atoi(roomId)
if err != nil {
l().Warn("parse room id error", err)
return
}
if !util.StringSliceContains(config.LiveRoom.History, roomId) {
config.LiveRoom.History = append(config.LiveRoom.History, roomId)
}
LiveClient = liveclient.NewBilibili(room)
LiveClient.Handler().Register(&event.EventHandler{
EventId: liveclient.EventMessageReceive,
Name: "controller.commandexecutor",
Handler: danmuCommandHandler,
})
LiveClient.Handler().RegisterA(
liveclient.EventMessageReceive,
"controller.danmu.handler",
danmuHandler)
l().Infof("setting live client for %s success", roomId)
}
func StartDanmuClient() {
LiveClient.Connect()
}
func ResetDanmuClient() {
if LiveClient != nil {
l().Infof("disconnect from current live client %s", LiveClient.ClientName())
LiveClient.Disconnect()
LiveClient.Handler().UnregisterAll()
LiveClient = nil
}
}
func AddPlaylist(pname string, uri string) *player.Playlist {
l().Infof("try add playlist %s with provider %s", uri, pname)
id, err := provider.FormatPlaylistUrl(pname, uri)
if err != nil || id == "" {
l().Warnf("fail to format %s playlist id for %s", uri, pname)
return nil
}
p := player.NewPlaylist(fmt.Sprintf("%s-%s", pname, id), player.PlaylistConfig{})
p.Meta = provider.Meta{
Name: pname,
Id: id,
}
PlaylistManager = append(PlaylistManager, p)
config.Player.Playlists = append(config.Player.Playlists, id)
config.Player.PlaylistsProvider = append(config.Player.PlaylistsProvider, pname)
return p
}
func RemovePlaylist(index int) {
l().Infof("Try to remove playlist.index=%d", index)
if index < 0 || index >= len(PlaylistManager) {
l().Warnf("playlist.index=%d not found", index)
return
}
if index == config.Player.PlaylistIndex {
l().Info("Delete current system playlist, reset system playlist to index = 0")
SetSystemPlaylist(0)
}
if index < config.Player.PlaylistIndex {
l().Debugf("Delete playlist before system playlist (index=%d), reduce system playlist index by 1", config.Player.PlaylistIndex)
config.Player.PlaylistIndex = config.Player.PlaylistIndex - 1
}
PlaylistManager = append(PlaylistManager[:index], PlaylistManager[index+1:]...)
config.Player.Playlists = append(config.Player.Playlists[:index], config.Player.Playlists[index+1:]...)
config.Player.PlaylistsProvider = append(config.Player.PlaylistsProvider[:index], config.Player.PlaylistsProvider[index+1:]...)
}
func SetSystemPlaylist(index int) {
l().Infof("try set system playlist to playlist.id=%d", index)
if index < 0 || index >= len(PlaylistManager) {
l().Warn("playlist.index=%d not found", index)
return
}
err := PreparePlaylist(PlaylistManager[index])
if err != nil {
return
}
medias := PlaylistManager[index].Playlist
config.Player.PlaylistIndex = index
ApplyUser(medias, player.PlaylistUser)
SystemPlaylist.Replace(medias)
}
func PreparePlaylistByIndex(index int) {
l().Infof("try prepare playlist.id=%d", index)
if index < 0 || index >= len(PlaylistManager) {
l().Warn("playlist.id=%d not found", index)
return
}
err := PreparePlaylist(PlaylistManager[index])
if err != nil {
return
}
}

View File

@@ -1,10 +0,0 @@
package controller
import (
"fmt"
"testing"
)
func TestController(t *testing.T) {
fmt.Println(LiveClient == nil)
}

View File

@@ -1,23 +0,0 @@
package controller
import (
"AynaLivePlayer/event"
"AynaLivePlayer/liveclient"
)
var DanmuHandlers []DanmuHandler
type DanmuHandler interface {
Execute(anmu *liveclient.DanmuMessage)
}
func AddDanmuHandler(handlers ...DanmuHandler) {
DanmuHandlers = append(DanmuHandlers, handlers...)
}
func danmuHandler(event *event.Event) {
danmu := event.Data.(*liveclient.DanmuMessage)
for _, cmd := range DanmuHandlers {
cmd.Execute(danmu)
}
}

View File

@@ -1,70 +0,0 @@
package controller
import (
"AynaLivePlayer/config"
"AynaLivePlayer/liveclient"
"AynaLivePlayer/player"
"AynaLivePlayer/provider"
"fmt"
)
var MainPlayer *player.Player
var UserPlaylist *player.Playlist
var History *player.Playlist
var HistoryUser *player.User
var SystemPlaylist *player.Playlist
var LiveClient liveclient.LiveClient
var PlaylistManager []*player.Playlist
var CurrentLyric *player.Lyric
var CurrentMedia *player.Media
func Initialize() {
MainPlayer = player.NewPlayer()
SetAudioDevice(config.Player.AudioDevice)
SetVolume(config.Player.Volume)
UserPlaylist = player.NewPlaylist("user", player.PlaylistConfig{RandomNext: false})
SystemPlaylist = player.NewPlaylist("system", player.PlaylistConfig{RandomNext: config.Player.PlaylistRandom})
PlaylistManager = make([]*player.Playlist, 0)
CurrentLyric = player.NewLyric("")
loadPlaylists()
History = player.NewPlaylist("history", player.PlaylistConfig{RandomNext: false})
HistoryUser = &player.User{Name: "History"}
MainPlayer.ObserveProperty("idle-active", handleMpvIdlePlayNext)
UserPlaylist.Handler.RegisterA(player.EventPlaylistInsert, "controller.playnextwhenadd", handlePlaylistAdd)
MainPlayer.ObserveProperty("time-pos", handleLyricUpdate)
MainPlayer.Start()
}
func loadPlaylists() {
l().Info("Loading playlists ", config.Player.Playlists, config.Player.PlaylistsProvider)
if len(config.Player.Playlists) != len(config.Player.Playlists) {
l().Warn("playlist id and provider does not have same length")
return
}
for i := 0; i < len(config.Player.Playlists); i++ {
pname := config.Player.PlaylistsProvider[i]
id := config.Player.Playlists[i]
p := player.NewPlaylist(fmt.Sprintf("%s-%s", pname, id), player.PlaylistConfig{})
p.Meta = provider.Meta{
Name: pname,
Id: id,
}
PlaylistManager = append(PlaylistManager, p)
}
if config.Player.PlaylistIndex < 0 || config.Player.PlaylistIndex >= len(config.Player.Playlists) {
l().Warn("playlist index did not find")
return
}
go func() {
c := config.Player.PlaylistIndex
err := PreparePlaylist(PlaylistManager[c])
if err != nil {
return
}
SetSystemPlaylist(c)
}()
}

View File

@@ -1,35 +0,0 @@
package controller
import (
"AynaLivePlayer/config"
"AynaLivePlayer/event"
"AynaLivePlayer/player"
"github.com/aynakeya/go-mpv"
)
func handleMpvIdlePlayNext(property *mpv.EventProperty) {
isIdle := property.Data.(mpv.Node).Value.(bool)
if isIdle {
l().Info("mpv went idle, try play next")
PlayNext()
}
}
func handlePlaylistAdd(event *event.Event) {
if MainPlayer.IsIdle() {
PlayNext()
return
}
if config.Player.SkipPlaylist && CurrentMedia != nil && CurrentMedia.User == player.PlaylistUser {
PlayNext()
return
}
}
func handleLyricUpdate(property *mpv.EventProperty) {
if property.Data == nil {
return
}
t := property.Data.(mpv.Node).Value.(float64)
CurrentLyric.Update(t)
}

View File

@@ -1,130 +0,0 @@
package controller
import (
"AynaLivePlayer/config"
"AynaLivePlayer/player"
"AynaLivePlayer/provider"
)
func PlayNext() {
l().Info("try to play next possible media")
if UserPlaylist.Size() == 0 && SystemPlaylist.Size() == 0 {
return
}
var media *player.Media
if UserPlaylist.Size() != 0 {
media = UserPlaylist.Pop()
} else if SystemPlaylist.Size() != 0 {
media = SystemPlaylist.Next()
}
Play(media)
}
func Play(media *player.Media) {
l().Infof("prepare media %s", media.Title)
err := PrepareMedia(media)
if err != nil {
l().Warn("prepare media failed. try play next")
PlayNext()
return
}
CurrentMedia = media
AddToHistory(media)
if err := MainPlayer.Play(media); err != nil {
l().Warn("play failed", err)
return
}
CurrentLyric.Reload(media.Lyric)
// reset
media.Url = ""
}
func Add(keyword string, user interface{}) {
media := MediaMatch(keyword)
if media == nil {
medias, err := Search(keyword)
if err != nil {
l().Warnf("search for %s, got error %s", keyword, err)
return
}
if len(medias) == 0 {
l().Info("search for %s, got no result", keyword)
return
}
media = medias[0]
}
media.User = user
l().Infof("add media %s (%s)", media.Title, media.Artist)
UserPlaylist.Insert(-1, media)
}
func AddWithProvider(keyword string, pname string, user interface{}) {
media := provider.MatchMedia(pname, keyword)
if media == nil {
medias, err := provider.Search(pname, keyword)
if err != nil {
l().Warnf("search for %s, got error %s", keyword, err)
return
}
if len(medias) == 0 {
l().Infof("search for %s, got no result", keyword)
return
}
media = medias[0]
}
media.User = user
l().Infof("add media %s (%s)", media.Title, media.Artist)
UserPlaylist.Insert(-1, media)
}
func Seek(position float64, absolute bool) {
if err := MainPlayer.Seek(position, absolute); err != nil {
l().Warnf("seek to position %f (%t) failed, %s", position, absolute, err)
}
}
func Toggle() (b bool) {
var err error
if MainPlayer.IsPaused() {
err = MainPlayer.Unpause()
b = false
} else {
err = MainPlayer.Pause()
b = true
}
if err != nil {
l().Warn("toggle failed", err)
}
return
}
func SetVolume(volume float64) {
if MainPlayer.SetVolume(volume) != nil {
l().Warnf("set mpv volume to %f failed", volume)
return
}
config.Player.Volume = volume
}
func Destroy() {
MainPlayer.Stop()
}
func GetAudioDevices() []player.AudioDevice {
dl, err := MainPlayer.GetAudioDeviceList()
if err != nil {
return make([]player.AudioDevice, 0)
}
return dl
}
func SetAudioDevice(device string) {
l().Infof("set audio device to %s", device)
if err := MainPlayer.SetAudioDevice(device); err != nil {
l().Warnf("set mpv audio device to %s failed, %s", device, err)
MainPlayer.SetAudioDevice("auto")
config.Player.AudioDevice = "auto"
return
}
config.Player.AudioDevice = device
}

View File

@@ -1,27 +0,0 @@
package controller
import "AynaLivePlayer/player"
func AddToHistory(media *player.Media) {
l().Tracef("add media %s (%s) to history", media.Title, media.Artist)
media = media.Copy()
// reset url for future use
media.Url = ""
if History.Size() >= 1024 {
History.Replace([]*player.Media{})
}
History.Push(media)
return
}
func ToHistoryMedia(media *player.Media) *player.Media {
media = media.Copy()
media.User = HistoryUser
return media
}
func ToSystemMedia(media *player.Media) *player.Media {
media = media.Copy()
media.User = player.SystemUser
return media
}

View File

@@ -1,30 +0,0 @@
package controller
type Plugin interface {
Name() string
Enable() error
Disable() error
}
func LoadPlugin(plugin Plugin) {
l().Info("Loading plugin: " + plugin.Name())
if err := plugin.Enable(); err != nil {
l().Warnf("Failed to load plugin: %s, %s", plugin.Name(), err)
}
}
func LoadPlugins(plugins ...Plugin) {
for _, plugin := range plugins {
LoadPlugin(plugin)
}
}
func ClosePlugins(plugins ...Plugin) {
for _, plugin := range plugins {
err := plugin.Disable()
if err != nil {
l().Warnf("Failed to close plugin: %s, %s", plugin.Name(), err)
return
}
}
}

View File

@@ -1,95 +0,0 @@
package controller
import (
"AynaLivePlayer/config"
"AynaLivePlayer/player"
"AynaLivePlayer/provider"
)
func PrepareMedia(media *player.Media) error {
var err error
if media.Title == "" || !media.Cover.Exists() {
l().Trace("fetching media info")
if err = provider.UpdateMedia(media); err != nil {
l().Warn("fail to prepare media when fetch info", err)
return err
}
}
if media.Url == "" {
l().Trace("fetching media url")
if err = provider.UpdateMediaUrl(media); err != nil {
l().Warn("fail to prepare media when url", err)
return err
}
}
if media.Lyric == "" {
l().Trace("fetching media lyric")
if err = provider.UpdateMediaLyric(media); err != nil {
l().Warn("fail to prepare media when lyric", err)
}
}
return nil
}
func MediaMatch(keyword string) *player.Media {
l().Infof("Match media for %s", keyword)
for _, p := range config.Provider.Priority {
if pr, ok := provider.Providers[p]; ok {
m := pr.MatchMedia(keyword)
if m == nil {
continue
}
if err := provider.UpdateMedia(m); err == nil {
return m
}
} else {
l().Warnf("Provider %s not exist", p)
}
}
return nil
}
func Search(keyword string) ([]*player.Media, error) {
l().Infof("Search for %s", keyword)
for _, p := range config.Provider.Priority {
if pr, ok := provider.Providers[p]; ok {
r, err := pr.Search(keyword)
if err != nil {
l().Warn("Provider %s return err", err)
continue
}
return r, err
} else {
l().Warnf("Provider %s not exist", p)
}
}
return nil, provider.ErrorNoSuchProvider
}
func SearchWithProvider(keyword string, p string) ([]*player.Media, error) {
l().Infof("Search for %s using %s", keyword, p)
if pr, ok := provider.Providers[p]; ok {
r, err := pr.Search(keyword)
return r, err
}
l().Warnf("Provider %s not exist", p)
return nil, provider.ErrorNoSuchProvider
}
func ApplyUser(medias []*player.Media, user interface{}) {
for _, m := range medias {
m.User = user
}
}
func PreparePlaylist(playlist *player.Playlist) error {
l().Debug("Prepare playlist ", playlist.Meta.(provider.Meta))
medias, err := provider.GetPlaylist(playlist.Meta.(provider.Meta))
if err != nil {
l().Warn("prepare playlist failed ", err)
return err
}
ApplyUser(medias, player.SystemUser)
playlist.Replace(medias)
return nil
}

56
core/events/event.go Normal file
View File

@@ -0,0 +1,56 @@
package events
//const (
// EventPlay string = "player.play"
// EventPlayed string = "player.played"
// EventPlaylistPreInsert string = "playlist.insert.pre"
// EventPlaylistInsert string = "playlist.insert.after"
// EventPlaylistUpdate string = "playlist.update"
// EventLyricUpdate string = "lyric.update"
// EventLyricReload string = "lyric.reload"
//)
const ErrorUpdate = "update.error"
type ErrorUpdateEvent struct {
Error error
}
//
//func EventPlayerPropertyUpdate(property model.PlayerProperty) string {
// return string("player.property.update." + string(property))
//}
//
//type PlaylistInsertEvent struct {
// Playlist *model.Playlist
// Index int
// Media *model.Media
//}
//
//type PlaylistUpdateEvent struct {
// Playlist *model.Playlist // Playlist is a copy of the playlist
//}
//
//type PlayEvent struct {
// Media *model.Media
//}
//
//type LyricUpdateEvent struct {
// Lyrics *model.Lyric
// Time float64
// Lyric *model.LyricContext
//}
//
//type LyricReloadEvent struct {
// Lyrics *model.Lyric
//}
//
//type PlayerPropertyUpdateEvent struct {
// Property model.PlayerProperty
// Value model.PlayerPropertyValue
//}
//
//type LiveRoomStatusUpdateEvent struct {
// RoomTitle string
// Status bool
//}

7
core/events/gui.go Normal file
View File

@@ -0,0 +1,7 @@
package events
const GUISetPlayerWindowOpenCmd = "cmd.gui.player_window.op"
type GUISetPlayerWindowOpenCmdEvent struct {
SetOpen bool
}

73
core/events/liveroom.go Normal file
View File

@@ -0,0 +1,73 @@
package events
import (
"AynaLivePlayer/core/model"
liveroomsdk "github.com/AynaLivePlayer/liveroom-sdk"
)
//const (
// LiveRoomStatusChange string = "liveclient.status.change"
// LiveRoomMessageReceive string = "liveclient.message.receive"
//)
//
//type StatusChangeEvent struct {
// Connected bool
// Client adapter.LiveClient
//}
const LiveRoomAddCmd = "cmd.liveroom.add"
type LiveRoomAddCmdEvent struct {
Title string
Provider string
RoomKey string
}
const LiveRoomProviderUpdate = "update.liveroom.provider"
type LiveRoomProviderUpdateEvent struct {
Providers []model.LiveRoomProviderInfo
}
const LiveRoomRemoveCmd = "cmd.liveroom.remove"
type LiveRoomRemoveCmdEvent struct {
Identifier string
}
const LiveRoomRoomsUpdate = "update.liveroom.rooms"
type LiveRoomRoomsUpdateEvent struct {
Rooms []model.LiveRoom
}
const LiveRoomStatusUpdate = "update.liveroom.status"
type LiveRoomStatusUpdateEvent struct {
Room model.LiveRoom
}
const LiveRoomConfigChangeCmd = "cmd.liveroom.config.change"
type LiveRoomConfigChangeCmdEvent struct {
Identifier string
Config model.LiveRoomConfig
}
const LiveRoomOperationCmd = "cmd.liveroom.operation"
type LiveRoomOperationCmdEvent struct {
Identifier string
SetConnect bool // connect or disconnect
}
const LiveRoomOperationFinish = "update.liveroom.operation"
type LiveRoomOperationFinishEvent struct {
}
const LiveRoomMessageReceive = "update.liveroom.message"
type LiveRoomMessageReceiveEvent struct {
Message *liveroomsdk.Message
}

78
core/events/mapping.go Normal file
View File

@@ -0,0 +1,78 @@
package events
import (
"AynaLivePlayer/core/model"
"encoding/json"
"errors"
"reflect"
)
var EventsMapping = map[string]any{
LiveRoomAddCmd: LiveRoomAddCmdEvent{},
LiveRoomProviderUpdate: LiveRoomProviderUpdateEvent{},
LiveRoomRemoveCmd: LiveRoomRemoveCmdEvent{},
LiveRoomRoomsUpdate: LiveRoomRoomsUpdateEvent{},
LiveRoomStatusUpdate: LiveRoomStatusUpdateEvent{},
LiveRoomConfigChangeCmd: LiveRoomConfigChangeCmdEvent{},
LiveRoomOperationCmd: LiveRoomOperationCmdEvent{},
PlayerVolumeChangeCmd: PlayerVolumeChangeCmdEvent{},
PlayerPlayCmd: PlayerPlayCmdEvent{},
PlayerPlayErrorUpdate: PlayerPlayErrorUpdateEvent{},
PlayerSeekCmd: PlayerSeekCmdEvent{},
PlayerToggleCmd: PlayerToggleCmdEvent{},
PlayerSetPauseCmd: PlayerSetPauseCmdEvent{},
PlayerPlayNextCmd: PlayerPlayNextCmdEvent{},
PlayerLyricRequestCmd: PlayerLyricRequestCmdEvent{},
PlayerLyricReload: PlayerLyricReloadEvent{},
PlayerLyricPosUpdate: PlayerLyricPosUpdateEvent{},
PlayerPlayingUpdate: PlayerPlayingUpdateEvent{},
PlayerPropertyPauseUpdate: PlayerPropertyPauseUpdateEvent{},
PlayerPropertyPercentPosUpdate: PlayerPropertyPercentPosUpdateEvent{},
PlayerPropertyStateUpdate: PlayerPropertyStateUpdateEvent{},
PlayerPropertyTimePosUpdate: PlayerPropertyTimePosUpdateEvent{},
PlayerPropertyDurationUpdate: PlayerPropertyDurationUpdateEvent{},
PlayerPropertyVolumeUpdate: PlayerPropertyVolumeUpdateEvent{},
PlayerVideoPlayerSetWindowHandleCmd: PlayerVideoPlayerSetWindowHandleCmdEvent{},
PlayerSetAudioDeviceCmd: PlayerSetAudioDeviceCmdEvent{},
PlayerAudioDeviceUpdate: PlayerAudioDeviceUpdateEvent{},
PlaylistManagerSetSystemCmd: PlaylistManagerSetSystemCmdEvent{},
PlaylistManagerSystemUpdate: PlaylistManagerSystemUpdateEvent{},
PlaylistManagerRefreshCurrentCmd: PlaylistManagerRefreshCurrentCmdEvent{},
PlaylistManagerGetCurrentCmd: PlaylistManagerGetCurrentCmdEvent{},
PlaylistManagerCurrentUpdate: PlaylistManagerCurrentUpdateEvent{},
PlaylistManagerInfoUpdate: PlaylistManagerInfoUpdateEvent{},
PlaylistManagerAddPlaylistCmd: PlaylistManagerAddPlaylistCmdEvent{},
PlaylistManagerRemovePlaylistCmd: PlaylistManagerRemovePlaylistCmdEvent{},
MediaProviderUpdate: MediaProviderUpdateEvent{},
SearchCmd: SearchCmdEvent{},
SearchResultUpdate: SearchResultUpdateEvent{},
GUISetPlayerWindowOpenCmd: GUISetPlayerWindowOpenCmdEvent{},
}
func init() {
for _, v := range []model.PlaylistID{model.PlaylistIDSystem, model.PlaylistIDPlayer, model.PlaylistIDHistory} {
EventsMapping[PlaylistDetailUpdate(v)] = PlaylistDetailUpdateEvent{}
EventsMapping[PlaylistMoveCmd(v)] = PlaylistMoveCmdEvent{}
EventsMapping[PlaylistSetIndexCmd(v)] = PlaylistSetIndexCmdEvent{}
EventsMapping[PlaylistDeleteCmd(v)] = PlaylistDeleteCmdEvent{}
EventsMapping[PlaylistInsertCmd(v)] = PlaylistInsertCmdEvent{}
EventsMapping[PlaylistInsertUpdate(v)] = PlaylistInsertUpdateEvent{}
EventsMapping[PlaylistNextCmd(v)] = PlaylistNextCmdEvent{}
EventsMapping[PlaylistNextUpdate(v)] = PlaylistNextUpdateEvent{}
EventsMapping[PlaylistModeChangeCmd(v)] = PlaylistModeChangeCmdEvent{}
EventsMapping[PlaylistModeChangeUpdate(v)] = PlaylistModeChangeUpdateEvent{}
}
}
func UnmarshalEventData(eventId string, data []byte) (any, error) {
val, ok := EventsMapping[eventId]
if !ok {
return nil, errors.New("event id not found")
}
newVal := reflect.New(reflect.TypeOf(val))
err := json.Unmarshal(data, newVal.Interface())
if err != nil {
return nil, err
}
return newVal.Elem().Interface(), nil
}

View File

@@ -0,0 +1,22 @@
package events
import (
"encoding/json"
"github.com/stretchr/testify/require"
"testing"
)
func TestUnmarshalEventData(t *testing.T) {
eventData := LiveRoomAddCmdEvent{
Title: "test",
Provider: "asdfasd",
RoomKey: "asdfasdf",
}
data, err := json.Marshal(eventData)
require.NoError(t, err)
val, err := UnmarshalEventData(LiveRoomAddCmd, data)
require.NoError(t, err)
resultData, ok := val.(LiveRoomAddCmdEvent)
require.True(t, ok)
require.Equal(t, eventData, resultData)
}

View File

@@ -0,0 +1,49 @@
package events
import (
"AynaLivePlayer/core/model"
)
const PlayerVolumeChangeCmd = "cmd.player.op.change_volume"
type PlayerVolumeChangeCmdEvent struct {
Volume float64 // Volume from 0-100
}
const PlayerPlayCmd = "cmd.player.op.play"
type PlayerPlayCmdEvent struct {
Media model.Media
}
const PlayerPlayErrorUpdate = "update.player.play.error"
type PlayerPlayErrorUpdateEvent struct {
Error error
}
const PlayerSeekCmd = "cmd.player.op.seek"
type PlayerSeekCmdEvent struct {
Position float64
// Absolute is the seek mode.
// if absolute = true : position is the time in second
// if absolute = false: position is in percentage eg 0.1 0.2
Absolute bool
}
const PlayerToggleCmd = "cmd.player.op.toggle"
type PlayerToggleCmdEvent struct {
}
const PlayerSetPauseCmd = "cmd.player.op.pause"
type PlayerSetPauseCmdEvent struct {
Pause bool
}
const PlayerPlayNextCmd = "cmd.player.op.next"
type PlayerPlayNextCmdEvent struct {
}

View File

@@ -0,0 +1,23 @@
package events
import "github.com/AynaLivePlayer/miaosic"
const PlayerLyricRequestCmd = "cmd.player.lyric.request"
type PlayerLyricRequestCmdEvent struct {
}
const PlayerLyricReload = "update.player.lyric.reload"
type PlayerLyricReloadEvent struct {
Lyrics miaosic.Lyrics
}
const PlayerLyricPosUpdate = "update.player.lyric.pos"
type PlayerLyricPosUpdateEvent struct {
Time float64
CurrentIndex int // -1 means no lyric
CurrentLine miaosic.LyricLine
Total int // total lyric count
}

View File

@@ -0,0 +1,46 @@
package events
import "AynaLivePlayer/core/model"
const PlayerPlayingUpdate = "update.player.playing"
type PlayerPlayingUpdateEvent struct {
Media model.Media
Removed bool // if no media is playing, removed is true
}
const PlayerPropertyPauseUpdate = "update.player.property.pause"
type PlayerPropertyPauseUpdateEvent struct {
Paused bool
}
const PlayerPropertyPercentPosUpdate = "update.player.property.percent_pos"
type PlayerPropertyPercentPosUpdateEvent struct {
PercentPos float64
}
const PlayerPropertyStateUpdate = "update.player.property.state"
type PlayerPropertyStateUpdateEvent struct {
State model.PlayerState
}
const PlayerPropertyTimePosUpdate = "update.player.property.time_pos"
type PlayerPropertyTimePosUpdateEvent struct {
TimePos float64 // Time in seconds
}
const PlayerPropertyDurationUpdate = "update.player.property.duration"
type PlayerPropertyDurationUpdateEvent struct {
Duration float64 // Duration in seconds
}
const PlayerPropertyVolumeUpdate = "update.player.property.volume"
type PlayerPropertyVolumeUpdateEvent struct {
Volume float64 // Volume from 0-100
}

View File

@@ -0,0 +1,22 @@
package events
import "AynaLivePlayer/core/model"
const PlayerVideoPlayerSetWindowHandleCmd = "cmd.player.videoplayer.set_window_handle"
type PlayerVideoPlayerSetWindowHandleCmdEvent struct {
Handle uintptr
}
const PlayerSetAudioDeviceCmd = "cmd.player.set_audio_device"
type PlayerSetAudioDeviceCmdEvent struct {
Device string
}
const PlayerAudioDeviceUpdate = "update.player.audio_device"
type PlayerAudioDeviceUpdateEvent struct {
Current string
Devices []model.AudioDevice
}

88
core/events/playlist.go Normal file
View File

@@ -0,0 +1,88 @@
package events
import (
"AynaLivePlayer/core/model"
)
func PlaylistDetailUpdate(id model.PlaylistID) string {
return string("update.playlist.detail." + id)
}
type PlaylistDetailUpdateEvent struct {
Medias []model.Media
}
func PlaylistMoveCmd(id model.PlaylistID) string {
return string("cmd.playlist.move." + id)
}
type PlaylistMoveCmdEvent struct {
From int
To int
}
func PlaylistSetIndexCmd(id model.PlaylistID) string {
return string("cmd.playlist.setindex." + id)
}
type PlaylistSetIndexCmdEvent struct {
Index int
}
func PlaylistDeleteCmd(id model.PlaylistID) string {
return string("cmd.playlist.delete." + id)
}
type PlaylistDeleteCmdEvent struct {
Index int
}
func PlaylistInsertCmd(id model.PlaylistID) string {
return string("cmd.playlist.insert." + id)
}
type PlaylistInsertCmdEvent struct {
Position int // position to insert, -1 means last one
Media model.Media
}
func PlaylistInsertUpdate(id model.PlaylistID) string {
return string("update.playlist.insert." + id)
}
type PlaylistInsertUpdateEvent struct {
Position int // position to insert, -1 means last one
Media model.Media
}
func PlaylistNextCmd(id model.PlaylistID) string {
return string("cmd.playlist.next." + id)
}
type PlaylistNextCmdEvent struct {
Remove bool // remove the media after next
}
func PlaylistNextUpdate(id model.PlaylistID) string {
return string("update.playlist.next." + id)
}
type PlaylistNextUpdateEvent struct {
Media model.Media
}
func PlaylistModeChangeCmd(id model.PlaylistID) string {
return string("cmd.playlist.mode." + id)
}
type PlaylistModeChangeCmdEvent struct {
Mode model.PlaylistMode
}
func PlaylistModeChangeUpdate(id model.PlaylistID) string {
return string("update.playlist.mode." + id)
}
type PlaylistModeChangeUpdateEvent struct {
Mode model.PlaylistMode
}

54
core/events/playlists.go Normal file
View File

@@ -0,0 +1,54 @@
package events
import (
"AynaLivePlayer/core/model"
)
const PlaylistManagerSetSystemCmd = "cmd.playlist.manager.set.system"
type PlaylistManagerSetSystemCmdEvent struct {
PlaylistID string
}
const PlaylistManagerSystemUpdate = "update.playlist.manager.system"
type PlaylistManagerSystemUpdateEvent struct {
Info model.PlaylistInfo
}
const PlaylistManagerRefreshCurrentCmd = "cmd.playlist.manager.refresh.current"
type PlaylistManagerRefreshCurrentCmdEvent struct {
PlaylistID string
}
const PlaylistManagerGetCurrentCmd = "cmd.playlist.manager.get.current"
type PlaylistManagerGetCurrentCmdEvent struct {
PlaylistID string
}
const PlaylistManagerCurrentUpdate = "update.playlist.manager.current"
type PlaylistManagerCurrentUpdateEvent struct {
Medias []model.Media
}
const PlaylistManagerInfoUpdate = "update.playlist.manager.info"
type PlaylistManagerInfoUpdateEvent struct {
Playlists []model.PlaylistInfo
}
const PlaylistManagerAddPlaylistCmd = "cmd.playlist.manager.add"
type PlaylistManagerAddPlaylistCmdEvent struct {
Provider string
URL string
}
const PlaylistManagerRemovePlaylistCmd = "cmd.playlist.manager.remove"
type PlaylistManagerRemovePlaylistCmdEvent struct {
PlaylistID string
}

7
core/events/provider.go Normal file
View File

@@ -0,0 +1,7 @@
package events
const MediaProviderUpdate = "update.media.provider.update"
type MediaProviderUpdateEvent struct {
Providers []string
}

18
core/events/search.go Normal file
View File

@@ -0,0 +1,18 @@
package events
import (
"AynaLivePlayer/core/model"
)
const SearchCmd = "cmd.search"
type SearchCmdEvent struct {
Keyword string
Provider string
}
const SearchResultUpdate = "update.search_result"
type SearchResultUpdateEvent struct {
Medias []model.Media
}

15
core/events/updater.go Normal file
View File

@@ -0,0 +1,15 @@
package events
import "AynaLivePlayer/core/model"
const CheckUpdateCmd = "cmd.update.check"
type CheckUpdateCmdEvent struct {
}
const CheckUpdateResultUpdate = "update.update.check"
type CheckUpdateResultUpdateEvent struct {
HasUpdate bool
Info model.VersionInfo
}

35
core/model/application.go Normal file
View File

@@ -0,0 +1,35 @@
package model
import "fmt"
type VersionInfo struct {
Version Version
Info string
}
type Version uint32
func (v Version) String() string {
return fmt.Sprintf("%d.%d.%d", (v>>16)&0xff, (v>>8)&0xff, v&0xff)
}
func (v Version) Major() uint8 {
return uint8((v >> 16) & 0xff)
}
func (v Version) Minor() uint8 {
return uint8((v >> 8) & 0xff)
}
func (v Version) Patch() uint8 {
return uint8(v & 0xff)
}
func VersionFromString(s string) Version {
var major, minor, patch uint8
_, err := fmt.Sscanf(s, "%d.%d.%d", &major, &minor, &patch)
if err != nil {
return 0
}
return Version(major)<<16 | Version(minor)<<8 | Version(patch)
}

View File

@@ -0,0 +1,17 @@
package model
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestVersion(t *testing.T) {
v := Version(0x00010203)
assert.Equal(t, "1.2.3", v.String())
v2 := VersionFromString("1.2.3")
assert.Equal(t, v, v2)
v3 := VersionFromString("1.2.4")
assert.True(t, v3 > v2)
assert.False(t, v3 < v2)
assert.Equal(t, Version(0), VersionFromString(""))
}

26
core/model/liveroom.go Normal file
View File

@@ -0,0 +1,26 @@
package model
import "github.com/AynaLivePlayer/liveroom-sdk"
type LiveRoomConfig struct {
AutoConnect bool `json:"auto_connect"`
}
type LiveRoom struct {
LiveRoom liveroom.LiveRoom `json:"live_room"`
Config LiveRoomConfig `json:"config"`
Title string `json:"title"`
Status bool `json:"status"`
}
func (r *LiveRoom) DisplayName() string {
if r.Title != "" {
return r.Title
}
return r.LiveRoom.Identifier()
}
type LiveRoomProviderInfo struct {
Name string
Description string
}

37
core/model/media.go Normal file
View File

@@ -0,0 +1,37 @@
package model
import (
"github.com/AynaLivePlayer/liveroom-sdk"
"github.com/AynaLivePlayer/miaosic"
)
type User struct {
Name string
}
var PlaylistUser = User{Name: "Playlists"}
var SystemUser = User{Name: "System"}
type Media struct {
Info miaosic.MediaInfo
User interface{}
}
func (m *Media) IsLiveRoomUser() bool {
_, ok := m.User.(liveroom.User)
return ok
}
func (m *Media) ToUser() User {
if u, ok := m.User.(User); ok {
return u
}
return User{Name: m.DanmuUser().Username}
}
func (m *Media) DanmuUser() liveroom.User {
if u, ok := m.User.(liveroom.User); ok {
return u
}
return liveroom.User{}
}

30
core/model/player.go Normal file
View File

@@ -0,0 +1,30 @@
package model
type AudioDevice struct {
Name string
Description string
}
type PlayerState int
const (
PlayerStatePlaying PlayerState = iota
PlayerStateLoading
PlayerStateIdle
)
func (s PlayerState) NextState(next PlayerState) PlayerState {
if s == PlayerStatePlaying {
return next
}
if s == PlayerStateIdle {
return next
}
if s == PlayerStateLoading {
if next != PlayerStatePlaying {
return PlayerStateLoading
}
return next
}
return next
}

31
core/model/playlist.go Normal file
View File

@@ -0,0 +1,31 @@
package model
import "github.com/AynaLivePlayer/miaosic"
type PlaylistMode int
const (
PlaylistModeNormal PlaylistMode = iota
PlaylistModeRandom
PlaylistModeRepeat
)
type PlaylistID string
const (
PlaylistIDPlayer PlaylistID = "player"
PlaylistIDSystem PlaylistID = "system"
PlaylistIDHistory PlaylistID = "history"
)
type PlaylistInfo struct {
Meta miaosic.MetaData
Title string
}
func (p PlaylistInfo) DisplayName() string {
if p.Title != "" {
return p.Title
}
return p.Meta.ID()
}

7
core/model/plugin.go Normal file
View File

@@ -0,0 +1,7 @@
package model
type Plugin interface {
Name() string
Enable() error
Disable() error
}

View File

@@ -1,88 +0,0 @@
package event
import (
"AynaLivePlayer/logger"
"github.com/sirupsen/logrus"
"sync"
)
type EventId string
const MODULE_HANDLER = "EventHandler"
var eventLogger = logger.Logger.WithFields(logrus.Fields{
"Module": MODULE_HANDLER,
})
type Event struct {
Id EventId
Cancelled bool
Data interface{}
}
type EventHandlerFunc func(event *Event)
type EventHandler struct {
EventId EventId
Name string
Handler EventHandlerFunc
}
type Handler struct {
handlers map[string]*EventHandler
lock sync.RWMutex
}
func NewHandler() *Handler {
return &Handler{
handlers: make(map[string]*EventHandler),
}
}
func (h *Handler) Register(handler *EventHandler) {
h.lock.Lock()
defer h.lock.Unlock()
eventLogger.Tracef("register new handler id=%s,name=%s", handler.EventId, handler.Name)
h.handlers[handler.Name] = handler
}
func (h *Handler) RegisterA(id EventId, name string, handler EventHandlerFunc) {
h.Register(&EventHandler{
EventId: id,
Name: name,
Handler: handler,
})
}
func (h *Handler) UnregisterAll() {
h.lock.Lock()
defer h.lock.Unlock()
eventLogger.Trace("clear all handler")
h.handlers = make(map[string]*EventHandler)
}
func (h *Handler) Unregister(name string) {
h.lock.Lock()
defer h.lock.Unlock()
eventLogger.Tracef("unregister handler name=%s", name)
delete(h.handlers, name)
}
func (h *Handler) Call(event *Event) {
h.lock.RLock()
defer h.lock.RUnlock()
for _, eh := range h.handlers {
if eh.EventId == event.Id {
eventLogger.Tracef("handler name=%s called by event_id = %s", event.Id, eh.Name)
// todo: @3
go eh.Handler(event)
}
}
}
func (h *Handler) CallA(id EventId, data interface{}) {
h.Call(&Event{
Id: id,
Data: data,
})
}

10
global/global.go Normal file
View File

@@ -0,0 +1,10 @@
package global
import (
"AynaLivePlayer/pkg/eventbus"
"AynaLivePlayer/pkg/logger"
)
var Logger logger.ILogger = nil
var EventBus eventbus.Bus = nil

108
go.mod
View File

@@ -1,26 +1,94 @@
module AynaLivePlayer
go 1.16
go 1.23.0
require (
fyne.io/fyne/v2 v2.1.4
github.com/XiaoMengXinX/Music163Api-Go v0.1.26
github.com/antonfisher/nested-logrus-formatter v1.3.1
github.com/aynakeya/blivedm v0.1.3
github.com/aynakeya/go-mpv v0.0.4
github.com/dhowden/tag v0.0.0-20220618230019-adf36e896086
github.com/go-resty/resty/v2 v2.7.0
github.com/gorilla/websocket v1.5.0
github.com/jinzhu/copier v0.3.5
github.com/sirupsen/logrus v1.8.1
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/spf13/cast v1.3.1
github.com/tidwall/gjson v1.14.1
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459
gopkg.in/ini.v1 v1.66.4
)
toolchain go1.24.4
replace (
github.com/aynakeya/blivedm => D:\Repository\blivedm
github.com/aynakeya/go-mpv => D:\Repository\go-mpv
github.com/AynaLivePlayer/liveroom-sdk v0.1.0 => ./pkg/liveroom-sdk // submodule
github.com/AynaLivePlayer/miaosic v0.2.3 => ./pkg/miaosic // submodule
github.com/saltosystems/winrt-go => github.com/go-musicfox/winrt-go v0.1.4 // winrt with media foundation
)
require (
fyne.io/fyne/v2 v2.6.3
github.com/AynaLivePlayer/liveroom-sdk v0.1.0
github.com/AynaLivePlayer/miaosic v0.2.3
github.com/adrg/libvlc-go/v3 v3.1.6
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b
github.com/antonfisher/nested-logrus-formatter v1.3.1
github.com/aynakeya/go-mpv v0.0.8
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728
github.com/go-ole/go-ole v1.3.0
github.com/go-resty/resty/v2 v2.16.5
github.com/gorilla/websocket v1.5.3
github.com/mattn/go-colorable v0.1.14
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/saltosystems/winrt-go v0.0.0-20241223121953-98e32661f6ff
github.com/sirupsen/logrus v1.9.3
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/stretchr/testify v1.10.0
github.com/tidwall/gjson v1.18.0
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459
go.uber.org/zap v1.27.0
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
golang.org/x/sys v0.34.0
gopkg.in/ini.v1 v1.67.0
)
require (
fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58 // indirect
github.com/AynaLivePlayer/blivedm-go v0.0.0-20250629154348-690af765bfbc // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/PuerkitoBio/goquery v1.10.3 // indirect
github.com/XiaoMengXinX/Music163Api-Go v0.1.30 // indirect
github.com/abadojack/whatlanggo v1.0.1 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/aynakeya/deepcolor v1.0.3 // indirect
github.com/aynakeya/open-bilibili-live v0.0.7 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 // indirect
github.com/fredbi/uri v1.1.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fyne-io/gl-js v0.2.0 // indirect
github.com/fyne-io/glfw-js v0.3.0 // indirect
github.com/fyne-io/image v0.1.1 // indirect
github.com/fyne-io/oksvg v0.1.0 // indirect
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
github.com/go-text/render v0.2.0 // indirect
github.com/go-text/typesetting v0.2.1 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
github.com/hack-pad/safejs v0.1.0 // indirect
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
github.com/jinzhu/copier v0.4.0 // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/makiuchi-d/gozxing v0.1.1 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rymdport/portal v0.4.1 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
github.com/spf13/cast v1.9.2 // indirect
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/yuin/goldmark v1.7.8 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/image v0.24.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
//replace (
// github.com/aynakeya/blivedm => D:\Repository\blivedm
// github.com/aynakeya/go-mpv => D:\Repository\go-mpv
//)

315
go.sum
View File

@@ -1,132 +1,259 @@
fyne.io/fyne/v2 v2.1.4 h1:bt1+28++kAzRzPB0GM2EuSV4cnl8rXNX4cjfd8G06Rc=
fyne.io/fyne/v2 v2.1.4/go.mod h1:p+E/Dh+wPW8JwR2DVcsZ9iXgR9ZKde80+Y+40Is54AQ=
fyne.io/fyne/v2 v2.6.3 h1:cvtM2KHeRuH+WhtHiA63z5wJVBkQ9+Ay0UMl9PxFHyA=
fyne.io/fyne/v2 v2.6.3/go.mod h1:NGSurpRElVoI1G3h+ab2df3O5KLGh1CGbsMMcX0bPIs=
fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58 h1:eA5/u2XRd8OUkoMqEv3IBlFYSruNlXD8bRHDiqm0VNI=
fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
github.com/AynaLivePlayer/blivedm-go v0.0.0-20250629154348-690af765bfbc h1:t1fMdqUjB2lR9uuGQ9yWJ7LJ3h1hXhI+LhbTpElPueI=
github.com/AynaLivePlayer/blivedm-go v0.0.0-20250629154348-690af765bfbc/go.mod h1:u+JfexgX5pYrylIuC5zP3N/Ylp47K/xvl+ntpZtosuE=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I=
github.com/XiaoMengXinX/Music163Api-Go v0.1.26 h1:Nybor5okI8C0jzAiRvGfpLHdDrPqUbjx5kXWIZDX6pw=
github.com/XiaoMengXinX/Music163Api-Go v0.1.26/go.mod h1:kLU/CkLxKnEJFCge0URvQ0lHt6ImoG1/2aVeNbgV2RQ=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
github.com/XiaoMengXinX/Music163Api-Go v0.1.30 h1:MqRItDFtX1J0JTlFtwN2RwjsYMA7/g/+cTjcOJXy19s=
github.com/XiaoMengXinX/Music163Api-Go v0.1.30/go.mod h1:kLU/CkLxKnEJFCge0URvQ0lHt6ImoG1/2aVeNbgV2RQ=
github.com/abadojack/whatlanggo v1.0.1 h1:19N6YogDnf71CTHm3Mp2qhYfkRdyvbgwWdd2EPxJRG4=
github.com/abadojack/whatlanggo v1.0.1/go.mod h1:66WiQbSbJBIlOZMsvbKe5m6pzQovxCH9B/K8tQB2uoc=
github.com/adrg/libvlc-go/v3 v3.1.6 h1:Cm22w6xNMDdzYCW8koHgAvjonYm4xbPP5TrlVTtMdl4=
github.com/adrg/libvlc-go/v3 v3.1.6/go.mod h1:xJK0YD8cyMDejnrTFQinStE6RYCV1nlfS8KmqTpszSc=
github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY=
github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/antonfisher/nested-logrus-formatter v1.3.1 h1:NFJIr+pzwv5QLHTPyKz9UMEoHck02Q9L0FP13b/xSbQ=
github.com/antonfisher/nested-logrus-formatter v1.3.1/go.mod h1:6WTfyWFkBc9+zyBaKIqRrg/KwMqBbodBjgbHjDz7zjA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/aynakeya/deepcolor v1.0.3 h1:FKDVxGIiD4R2XSOhS6HwCWPPsmXgG3KKqja1nNNU7HE=
github.com/aynakeya/deepcolor v1.0.3/go.mod h1:9wdFsi0G4uAQlu58B2/eHBlGoQ8VkmSyPsK+bDZ+6dQ=
github.com/aynakeya/go-mpv v0.0.8 h1:Gtc7N0EuPqB5JEOdah9cvamSCZIE/vqTcV5MuUAPaCg=
github.com/aynakeya/go-mpv v0.0.8/go.mod h1:do6ImaEyt9dlQ7JRS/8ke+P9q4kGW8+Bf6j3faBQOfE=
github.com/aynakeya/open-bilibili-live v0.0.7 h1:em/IpFeExaUmOO+jSlhzjjppiuDflLuobzdMo96dtOo=
github.com/aynakeya/open-bilibili-live v0.0.7/go.mod h1:8aYl0767J4wgBkJ6kgOpOuj6FcFFL8GN3HRHGGjYpSk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dhowden/itl v0.0.0-20170329215456-9fbe21093131/go.mod h1:eVWQJVQ67aMvYhpkDwaH2Goy2vo6v8JCMfGXfQ9sPtw=
github.com/dhowden/plist v0.0.0-20141002110153-5db6e0d9931a/go.mod h1:sLjdR6uwx3L6/Py8F+QgAfeiuY87xuYGwCDqRFrvCzw=
github.com/dhowden/tag v0.0.0-20220618230019-adf36e896086 h1:ORubSQoKnncsBnR4zD9CuYFJCPOCuSNEpWEZrDdBXkc=
github.com/dhowden/tag v0.0.0-20220618230019-adf36e896086/go.mod h1:Z3Lomva4pyMWYezjMAU5QWRh0p1VvO4199OHlFnyKkM=
github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 h1:FDqhDm7pcsLhhWl1QtD8vlzI4mm59llRvNzrFg6/LAA=
github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3/go.mod h1:CzM2G82Q9BDUvMTGHnXf/6OExw/Dz2ivDj48nVg7Lg8=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f h1:s0O46d8fPwk9kU4k1jj76wBquMVETx7uveQD9MCIQoU=
github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f/go.mod h1:wjpnOv6ONl2SuJSxqCPVaPZibGFdSci9HFocT9qtVYM=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211024062804-40e447a793be h1:Z28GdQBfKOL8tNHjvaDn3wHDO7AzTRkmAXvHvnopp98=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211024062804-40e447a793be/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-resty/resty/v2 v2.6.0/go.mod h1:PwvJS6hvaPkjtjNg9ph+VrSD92bi5Zq73w/BIH7cC3Q=
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff h1:W71vTCKoxtdXgnm1ECDFkfQnpdqAO00zzGXLA5yaEX8=
github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jackmordaunt/icns v0.0.0-20181231085925-4f16af745526/go.mod h1:UQkeMHVoNcyXYq9otUupF7/h/2tmHlhrS2zw7ZVvUqc=
github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg=
github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/josephspurrier/goversioninfo v0.0.0-20200309025242-14b0ab84c6ca/go.mod h1:eJTEwMjXb7kZ633hO3Ln9mBUCOjX2+FlTljvpl9SYdE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lucor/goinfo v0.0.0-20210802170112-c078a2b0f08b/go.mod h1:PRq09yoB+Q2OJReAmwzKivcYyremnibWGbK7WfftHzc=
github.com/mitchellh/panicwrap v1.0.0 h1:67zIyVakCIvcs69A0FGfZjBdPleaonSgGlXRSRlb6fE=
github.com/mitchellh/panicwrap v1.0.0/go.mod h1:pKvZHwWrZowLUzftuFq7coarnxbBXU4aQh3N0BJOeeA=
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 h1:OtSeLS5y0Uy01jaKK4mA/WVIYtpzVm63vLVAPzJXigg=
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8/go.mod h1:apkPC/CR3s48O2D7Y++n1XWEpgPNNCjXYga3PPbJe2E=
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fredbi/uri v1.1.0 h1:OqLpTXtyRg9ABReqvDGdJPqZUxs8cyBDOMXBbskCaB8=
github.com/fredbi/uri v1.1.0/go.mod h1:aYTUoAXBOq7BLfVJ8GnKmfcuURosB1xyHDIfWeC/iW4=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs=
github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk=
github.com/fyne-io/glfw-js v0.3.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk=
github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA=
github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM=
github.com/fyne-io/oksvg v0.1.0 h1:7EUKk3HV3Y2E+qypp3nWqMXD7mum0hCw2KEGhI1fnBw=
github.com/fyne-io/oksvg v0.1.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728 h1:RkGhqHxEVAvPM0/R+8g7XRwQnHatO0KAuVcwHo8q9W8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728/go.mod h1:SyRD8YfuKk+ZXlDqYiqe1qMSqjNgtHzBTG810KUagMc=
github.com/go-musicfox/winrt-go v0.1.4 h1:xg+7VKsIozGK8S4X4zNQ/3HNhg5yHWYaTE+Zs4jySaU=
github.com/go-musicfox/winrt-go v0.1.4/go.mod h1:CIltaIm7qaANUIvzr0Vmz71lmQMAIbGJ7cvgzX7FMfA=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc=
github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU=
github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8=
github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M=
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8=
github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/k0kubun/pp/v3 v3.5.0 h1:iYNlYA5HJAJvkD4ibuf9c8y6SHM0QFhaBuCqm1zHp0w=
github.com/k0kubun/pp/v3 v3.5.0/go.mod h1:5lzno5ZZeEeTV/Ky6vs3g6d1U3WarDrH8k240vMtGro=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/makiuchi-d/gozxing v0.1.1 h1:xxqijhoedi+/lZlhINteGbywIrewVdVv2wl9r5O9S1I=
github.com/makiuchi-d/gozxing v0.1.1/go.mod h1:eRIHbOjX7QWxLIDJoQuMLhuXg9LAuw6znsUtRkNw9DU=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rymdport/portal v0.4.1 h1:2dnZhjf5uEaeDjeF/yBIeeRo6pNI2QAKm7kq1w/kbnA=
github.com/rymdport/portal v0.4.1/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564 h1:HunZiaEKNGVdhTRQOVpMmj5MQnGnv+e8uZNu3xFLgyM=
github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564/go.mod h1:afMbS0qvv1m5tfENCwnOdZGOF8RGR/FsZ7bvBxQGZG4=
github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 h1:m59mIOBO4kfcNCEzJNy71UkeF4XIx2EVmL9KLwDQdmM=
github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU=
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/tidwall/gjson v1.8.0/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk=
github.com/tidwall/gjson v1.14.1 h1:iymTbGkQBhveq21bEvAQ81I0LEBork8BFe1CUZXdyuo=
github.com/tidwall/gjson v1.14.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459 h1:x9pIfbdIjnw+Ylb2vE27Gtqb7BDmfR+nLcJwvbJh98U=
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459/go.mod h1:nFvuG3SWu3VWqobG3cX8nt57wXU0OOFapeCs/8axIuM=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.3.8 h1:Nw158Q8QN+CPgTmVRByhVwapp8Mm1e2blinhmx4wx5E=
github.com/yuin/goldmark v1.3.8/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw=
golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb h1:pirldcYWx7rx7kE5r+9WsOXPXK0+WH5+uZ7uPmJ44uM=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4=
gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=

48
gui/component/button.go Normal file
View File

@@ -0,0 +1,48 @@
package component
import (
"AynaLivePlayer/gui/gutil"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/widget"
)
type AsyncButton struct {
widget.Button
}
func NewAsyncButton(label string, tapped func()) *AsyncButton {
b := &AsyncButton{
Button: widget.Button{
Text: label,
},
}
b.ExtendBaseWidget(b)
b.SetOnTapped(tapped)
return b
}
func NewAsyncButtonWithIcon(label string, icon fyne.Resource, tapped func()) *AsyncButton {
b := &AsyncButton{
Button: widget.Button{
Text: label,
Icon: icon,
},
}
b.ExtendBaseWidget(b)
b.SetOnTapped(tapped)
return b
}
func (b *AsyncButton) SetOnTapped(f func()) {
b.Button.OnTapped = func() {
b.Disable()
go func() {
f()
//time.Sleep(3 * time.Second)
// todo use fyne.Do after upgrade to v2.6.x
gutil.RunInFyneThread(b.Enable)
//b.Enable()
//fyne.Do(b.Enable)
}()
}
}

11
gui/component/check.go Normal file
View File

@@ -0,0 +1,11 @@
package component
import "fyne.io/fyne/v2/widget"
func NewCheckOneWayBinding(name string, val *bool, checked bool) *widget.Check {
check := widget.NewCheck(name, func(b bool) {
*val = b
})
check.SetChecked(checked)
return check
}

34
gui/component/entry.go Normal file
View File

@@ -0,0 +1,34 @@
package component
import (
"AynaLivePlayer/gui/xfyne"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/widget"
)
type Entry struct {
widget.Entry
OnKeyUp func(key *fyne.KeyEvent)
OnKeyDown func(key *fyne.KeyEvent)
}
func NewEntry() *Entry {
e := &Entry{}
e.ExtendBaseWidget(e)
xfyne.EntryDisableUndoRedo(&e.Entry)
return e
}
func (m *Entry) KeyUp(key *fyne.KeyEvent) {
m.Entry.KeyUp(key)
if m.OnKeyUp != nil {
m.OnKeyUp(key)
}
}
func (m *Entry) KeyDown(key *fyne.KeyEvent) {
m.Entry.KeyDown(key)
if m.OnKeyDown != nil {
m.OnKeyDown(key)
}
}

38
gui/component/slider.go Normal file
View File

@@ -0,0 +1,38 @@
package component
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/widget"
)
type SliderPlus struct {
widget.Slider
OnDragEnd func(value float64)
Dragging bool // during dragging
}
func NewSliderPlus(min, max float64) *SliderPlus {
slider := &SliderPlus{
Slider: widget.Slider{
Value: 0,
Min: min,
Max: max,
Step: 1,
Orientation: widget.Horizontal,
},
}
slider.ExtendBaseWidget(slider)
return slider
}
func (s *SliderPlus) DragEnd() {
if s.OnDragEnd != nil {
s.OnDragEnd(s.Value)
}
s.Dragging = false
}
func (s *SliderPlus) Dragged(e *fyne.DragEvent) {
s.Dragging = true
s.Slider.Dragged(e)
}

166
gui/component/split.go Normal file
View File

@@ -0,0 +1,166 @@
package component
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
type FixedSplit struct {
widget.BaseWidget
Offset float64
Horizontal bool
SeparatorThickness float32
Leading fyne.CanvasObject
Trailing fyne.CanvasObject
}
func NewFixedHSplitContainer(leading, trailing fyne.CanvasObject, offset float64) *FixedSplit {
return NewFixedSplitContainer(leading, trailing, true, offset)
}
func NewFixedVSplitContainer(top, bottom fyne.CanvasObject, offset float64) *FixedSplit {
return NewFixedSplitContainer(top, bottom, false, offset)
}
func NewFixedSplitContainer(leading, trailing fyne.CanvasObject, horizontal bool, offset float64) *FixedSplit {
s := &FixedSplit{
Offset: offset, // Sensible default, can be overridden with SetOffset
SeparatorThickness: theme.SeparatorThicknessSize(),
Horizontal: horizontal,
Leading: leading,
Trailing: trailing,
}
s.BaseWidget.ExtendBaseWidget(s)
return s
}
func (s *FixedSplit) CreateRenderer() fyne.WidgetRenderer {
s.BaseWidget.ExtendBaseWidget(s)
d := widget.NewSeparator()
return &fixedSplitContainerRenderer{
split: s,
divider: d,
objects: []fyne.CanvasObject{s.Leading, d, s.Trailing},
}
}
func (s *FixedSplit) SetOffset(offset float64) {
if s.Offset == offset {
return
}
s.Offset = offset
s.Refresh()
}
func (s *FixedSplit) SetSepThickness(thickness float32) {
if s.SeparatorThickness == thickness {
return
}
s.SeparatorThickness = thickness
s.Refresh()
}
type fixedSplitContainerRenderer struct {
split *FixedSplit
divider *widget.Separator
objects []fyne.CanvasObject
}
func (r *fixedSplitContainerRenderer) Destroy() {
}
func (r *fixedSplitContainerRenderer) Layout(size fyne.Size) {
var dividerPos, leadingPos, trailingPos fyne.Position
var dividerSize, leadingSize, trailingSize fyne.Size
if r.split.Horizontal {
lw, tw := r.computeSplitLengths(size.Width, r.split.Leading.MinSize().Width, r.split.Trailing.MinSize().Width)
leadingPos.X = 0
leadingSize.Width = lw
leadingSize.Height = size.Height
dividerPos.X = lw
dividerSize.Width = r.split.SeparatorThickness
dividerSize.Height = size.Height
trailingPos.X = lw + dividerSize.Width
trailingSize.Width = tw
trailingSize.Height = size.Height
} else {
lh, th := r.computeSplitLengths(size.Height, r.split.Leading.MinSize().Height, r.split.Trailing.MinSize().Height)
leadingPos.Y = 0
leadingSize.Width = size.Width
leadingSize.Height = lh
dividerPos.Y = lh
dividerSize.Width = size.Width
dividerSize.Height = r.split.SeparatorThickness
trailingPos.Y = lh + dividerSize.Height
trailingSize.Width = size.Width
trailingSize.Height = th
}
r.divider.Move(dividerPos)
r.divider.Resize(dividerSize)
r.split.Leading.Move(leadingPos)
r.split.Leading.Resize(leadingSize)
r.split.Trailing.Move(trailingPos)
r.split.Trailing.Resize(trailingSize)
canvas.Refresh(r.divider)
canvas.Refresh(r.split.Leading)
canvas.Refresh(r.split.Trailing)
}
func (r *fixedSplitContainerRenderer) MinSize() fyne.Size {
s := fyne.NewSize(0, 0)
for _, o := range r.objects {
min := o.MinSize()
if r.split.Horizontal {
s.Width += min.Width
s.Height = fyne.Max(s.Height, min.Height)
} else {
s.Width = fyne.Max(s.Width, min.Width)
s.Height += min.Height
}
}
return s
}
func (r *fixedSplitContainerRenderer) Objects() []fyne.CanvasObject {
return r.objects
}
func (r *fixedSplitContainerRenderer) Refresh() {
r.objects[0] = r.split.Leading
// [1] is divider which doesn't change
r.objects[2] = r.split.Trailing
r.Layout(r.split.Size())
canvas.Refresh(r.split)
}
func (r *fixedSplitContainerRenderer) computeSplitLengths(total, lMin, tMin float32) (float32, float32) {
available := float64(total - r.split.SeparatorThickness)
if available <= 0 {
return 0, 0
}
ld := float64(lMin)
tr := float64(tMin)
offset := r.split.Offset
min := ld / available
max := 1 - tr/available
if min <= max {
if offset < min {
offset = min
}
if offset > max {
offset = max
}
} else {
offset = ld / (ld + tr)
}
ld = offset * available
tr = available - ld
return float32(ld), float32(tr)
}

View File

@@ -1,12 +1,16 @@
package gui
import (
"AynaLivePlayer/config"
"AynaLivePlayer/controller"
"AynaLivePlayer/i18n"
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/gui/component"
"AynaLivePlayer/gui/gutil"
"AynaLivePlayer/pkg/config"
"AynaLivePlayer/pkg/eventbus"
"AynaLivePlayer/pkg/i18n"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/widget"
)
@@ -26,36 +30,108 @@ func (b *bascicConfig) CreatePanel() fyne.CanvasObject {
if b.panel != nil {
return b.panel
}
playerRandomCheck := widget.NewCheck(i18n.T("gui.config.basic.random_playlist.user"),
func(b bool) {
mode := model.PlaylistModeNormal
if b {
mode = model.PlaylistModeRandom
}
logger.Infof("Set player playlist mode to %d", mode)
_ = global.EventBus.Publish(events.PlaylistModeChangeCmd(model.PlaylistIDPlayer),
events.PlaylistModeChangeCmdEvent{
Mode: mode,
})
})
global.EventBus.Subscribe("", events.PlaylistModeChangeUpdate(model.PlaylistIDPlayer),
"gui.config.basic.random_playlist.player",
gutil.ThreadSafeHandler(func(event *eventbus.Event) {
data := event.Data.(events.PlaylistModeChangeUpdateEvent)
playerRandomCheck.SetChecked(data.Mode == model.PlaylistModeRandom)
}))
systemRandomCheck := widget.NewCheck(i18n.T("gui.config.basic.random_playlist.system"),
func(b bool) {
mode := model.PlaylistModeNormal
if b {
mode = model.PlaylistModeRandom
}
_ = global.EventBus.Publish(events.PlaylistModeChangeCmd(model.PlaylistIDSystem),
events.PlaylistModeChangeCmdEvent{
Mode: mode,
})
})
global.EventBus.Subscribe("", events.PlaylistModeChangeUpdate(model.PlaylistIDSystem),
"gui.config.basic.random_playlist.system",
gutil.ThreadSafeHandler(func(event *eventbus.Event) {
data := event.Data.(events.PlaylistModeChangeUpdateEvent)
systemRandomCheck.SetChecked(data.Mode == model.PlaylistModeRandom)
}))
randomPlaylist := container.NewHBox(
widget.NewLabel(i18n.T("gui.config.basic.random_playlist")),
widget.NewCheckWithData(
i18n.T("gui.config.basic.random_playlist.user"),
binding.BindBool(&controller.UserPlaylist.Config.RandomNext)),
widget.NewCheckWithData(
i18n.T("gui.config.basic.random_playlist.system"),
binding.BindBool(&controller.SystemPlaylist.Config.RandomNext)),
playerRandomCheck,
systemRandomCheck,
)
devices := controller.GetAudioDevices()
deviceDesc := make([]string, len(devices))
deviceDesc2Name := make(map[string]string)
for i, device := range devices {
deviceDesc[i] = device.Description
deviceDesc2Name[device.Description] = device.Name
}
deviceSel := widget.NewSelect(deviceDesc, func(s string) {
controller.SetAudioDevice(deviceDesc2Name[s])
deviceSel := widget.NewSelect(make([]string, 0), func(s string) {
name, ok := deviceDesc2Name[s]
if !ok {
return
}
_ = global.EventBus.Publish(events.PlayerSetAudioDeviceCmd, events.PlayerSetAudioDeviceCmdEvent{
Device: name,
})
})
deviceSel.Selected = config.Player.AudioDevice
global.EventBus.Subscribe("",
events.PlayerAudioDeviceUpdate,
"gui.config.basic.audio_device.update",
gutil.ThreadSafeHandler(func(event *eventbus.Event) {
data := event.Data.(events.PlayerAudioDeviceUpdateEvent)
devices := make([]string, len(data.Devices))
deviceDesc2Name = make(map[string]string)
currentDevice := ""
for i, device := range data.Devices {
devices[i] = device.Description
deviceDesc2Name[device.Description] = device.Name
if device.Name == data.Current {
currentDevice = device.Description
}
}
logger.Infof("update audio device. set current to %s (%s)", data.Current, deviceDesc2Name[data.Current])
deviceSel.Options = devices
deviceSel.Selected = currentDevice
deviceSel.Refresh()
}))
outputDevice := container.NewBorder(nil, nil,
widget.NewLabel(i18n.T("gui.config.basic.audio_device")), nil,
deviceSel)
skipPlaylist := container.NewHBox(
widget.NewLabel(i18n.T("gui.config.basic.skip_playlist")),
widget.NewCheckWithData(
i18n.T("gui.config.basic.skip_playlist.prompt"),
binding.BindBool(&config.Player.SkipPlaylist),
),
skipWhenErr := container.NewHBox(
widget.NewLabel(i18n.T("gui.config.basic.skip_when_error")),
component.NewCheckOneWayBinding(
i18n.T("gui.config.basic.skip_when_error.prompt"),
&config.General.PlayNextOnFail,
config.General.PlayNextOnFail),
)
b.panel = container.NewVBox(randomPlaylist, outputDevice, skipPlaylist)
checkUpdateBox := container.NewHBox(
widget.NewLabel(i18n.T("gui.config.basic.auto_check_update")),
component.NewCheckOneWayBinding(
i18n.T("gui.config.basic.auto_check_update.prompt"),
&config.General.AutoCheckUpdate,
config.General.AutoCheckUpdate),
)
checkUpdateBtn := widget.NewButton(i18n.T("gui.config.basic.check_update"), func() {
_ = global.EventBus.Publish(events.CheckUpdateCmd, events.CheckUpdateCmdEvent{})
})
useSysPlaylistBtn := container.NewHBox(
widget.NewLabel(i18n.T("gui.config.basic.use_system_playlist")),
component.NewCheckOneWayBinding(
i18n.T("gui.config.basic.use_system_playlist.prompt"),
&config.General.UseSystemPlaylist,
config.General.UseSystemPlaylist),
)
b.panel = container.NewVBox(randomPlaylist, useSysPlaylistBtn, skipWhenErr, outputDevice, checkUpdateBox, checkUpdateBtn)
return b.panel
}

View File

@@ -1,24 +1,24 @@
package gui
import (
"AynaLivePlayer/gui/component"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
type TestConfig struct {
var ConfigList = []ConfigLayout{
&bascicConfig{},
}
func (t *TestConfig) Title() string {
return "Test Title"
type ConfigLayout interface {
Title() string
Description() string
CreatePanel() fyne.CanvasObject
}
func (T *TestConfig) Description() string {
return "Test Description"
}
func (t *TestConfig) CreatePanel() fyne.CanvasObject {
return widget.NewLabel("asdf")
func AddConfigLayout(cfgs ...ConfigLayout) {
ConfigList = append(ConfigList, cfgs...)
}
func createConfigLayout() fyne.CanvasObject {
@@ -32,7 +32,7 @@ func createConfigLayout() fyne.CanvasObject {
return len(ConfigList)
},
func() fyne.CanvasObject {
return widget.NewLabel("AAAAAAAAAAAAAAAA")
return widget.NewLabel("")
},
func(id widget.ListItemID, object fyne.CanvasObject) {
object.(*widget.Label).SetText(ConfigList[id].Title())
@@ -44,16 +44,11 @@ func createConfigLayout() fyne.CanvasObject {
seg.Style.Alignment = fyne.TextAlignCenter
}
}
a := container.NewVScroll(ConfigList[id].CreatePanel())
content.Objects = []fyne.CanvasObject{
container.NewBorder(container.NewVBox(desc, widget.NewSeparator()), nil, nil, nil,
a),
container.NewVScroll(container.NewBorder(container.NewVBox(desc, widget.NewSeparator()), nil, nil, nil, ConfigList[id].CreatePanel())),
}
content.Refresh()
}
return container.NewBorder(
nil, nil,
container.NewHBox(entryList, widget.NewSeparator()), nil,
content)
return component.NewFixedSplitContainer(entryList, content, true, 0.23)
}

View File

@@ -1,69 +1,104 @@
package gui
import (
"AynaLivePlayer/config"
"AynaLivePlayer/i18n"
"AynaLivePlayer/logger"
"AynaLivePlayer/core/events"
"AynaLivePlayer/global"
"AynaLivePlayer/gui/gutil"
"AynaLivePlayer/pkg/config"
"AynaLivePlayer/pkg/eventbus"
"AynaLivePlayer/pkg/i18n"
"AynaLivePlayer/resource"
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"github.com/sirupsen/logrus"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
"os"
_logger "AynaLivePlayer/pkg/logger"
)
const MODULE_GUI = "GUI"
type ConfigLayout interface {
Title() string
Description() string
CreatePanel() fyne.CanvasObject
}
var App fyne.App
var MainWindow fyne.Window
var ConfigList = []ConfigLayout{&bascicConfig{}}
var playerWindow fyne.Window
var playerWindowHandle uintptr
func l() *logrus.Entry {
return logger.Logger.WithField("Module", MODULE_GUI)
var logger _logger.ILogger = nil
func black_magic() {
widget.RichTextStyleStrong.TextStyle.Bold = false
}
func Initialize() {
l().Info("Initializing GUI")
os.Setenv("FYNE_FONT", config.GetAssetPath("msyh.ttc"))
App = app.New()
MainWindow = App.NewWindow(fmt.Sprintf("%s Ver.%s", config.ProgramName, config.Version))
logger = global.Logger.WithPrefix("GUI")
black_magic()
logger.Info("Initializing GUI")
if config.General.CustomFonts != "" {
_ = os.Setenv("FYNE_FONT", config.GetAssetPath(config.General.CustomFonts))
}
App = app.NewWithID(config.ProgramName)
//App.Settings().SetTheme(&myTheme{})
MainWindow = App.NewWindow(getAppTitle())
tabs := container.NewAppTabs(
container.NewTabItem(i18n.T("gui.tab.player"),
newPaddedBoarder(nil, createPlayControllerV2(), nil, nil, createPlaylist()),
container.NewBorder(nil, createPlayControllerV2(), nil, nil, createPlaylist()),
),
container.NewTabItem(i18n.T("gui.tab.search"),
newPaddedBoarder(createSearchBar(), nil, nil, nil, createSearchList()),
container.NewBorder(createSearchBar(), nil, nil, nil, createSearchList()),
),
container.NewTabItem(i18n.T("gui.tab.room"),
newPaddedBoarder(createRoomController(), nil, nil, nil, createRoomLogger()),
container.NewBorder(nil, nil, createRoomSelector(), nil, createRoomController()),
),
container.NewTabItem(i18n.T("gui.tab.playlist"),
newPaddedBoarder(nil, nil, createPlaylists(), nil, createPlaylistMedias()),
container.NewBorder(nil, nil, createPlaylists(), nil, createPlaylistMedias()),
),
container.NewTabItem(i18n.T("gui.tab.history"),
newPaddedBoarder(nil, nil, nil, nil, createHistoryList()),
container.NewBorder(nil, nil, nil, nil, createHistoryList()),
),
container.NewTabItem(i18n.T("gui.tab.config"),
newPaddedBoarder(nil, nil, nil, nil, createConfigLayout()),
createConfigLayout(),
),
)
tabs.SetTabLocation(container.TabLocationTop)
MainWindow.SetIcon(fyne.NewStaticResource("icon", resource.ProgramIcon))
MainWindow.SetIcon(resource.ImageIcon)
MainWindow.SetContent(tabs)
//MainWindow.Resize(fyne.NewSize(1280, 720))
MainWindow.Resize(fyne.NewSize(960, 480))
//MainWindow.SetFixedSize(true)
}
MainWindow.Resize(fyne.NewSize(config.General.Width, config.General.Height))
func AddConfigLayout(cfgs ...ConfigLayout) {
ConfigList = append(ConfigList, cfgs...)
// todo: fix, window were created even if not show. this block gui from closing
// i can't create sub window before the main window shows.
// setupPlayerWindow()
// register error
global.EventBus.Subscribe("",
events.ErrorUpdate, "gui.show_error", gutil.ThreadSafeHandler(func(e *eventbus.Event) {
err := e.Data.(events.ErrorUpdateEvent).Error
logger.Warnf("gui received error event: %v, %v", err, err == nil)
if err == nil {
return
}
dialog.ShowError(err, MainWindow)
}))
checkUpdate()
MainWindow.SetFixedSize(config.General.FixedSize)
if config.General.ShowSystemTray {
setupSysTray()
} else {
MainWindow.SetCloseIntercept(
func() {
// todo: save twice i don;t care
_ = config.SaveToConfigFile(config.ConfigPath)
MainWindow.Close()
})
}
MainWindow.SetOnClosed(func() {
logger.Infof("GUI closing")
if playerWindow != nil {
logger.Infof("player window closing")
playerWindow.Close()
}
})
}

13
gui/gui_dev.go Normal file
View File

@@ -0,0 +1,13 @@
//go:build !nosource
package gui
import (
"AynaLivePlayer/core/model"
"AynaLivePlayer/pkg/config"
"fmt"
)
func getAppTitle() string {
return fmt.Sprintf("%s Ver %s - 测试版 仅供开发人员测试使用 请勿用于其他用途", config.ProgramName, model.Version(config.Version))
}

13
gui/gui_stable.go Normal file
View File

@@ -0,0 +1,13 @@
//go:build nosource
package gui
import (
"AynaLivePlayer/core/model"
"AynaLivePlayer/pkg/config"
"fmt"
)
func getAppTitle() string {
return fmt.Sprintf("%s Ver %s - 正式版", config.ProgramName, model.Version(config.Version))
}

21
gui/gutil/fyne.go Normal file
View File

@@ -0,0 +1,21 @@
package gutil
import (
"AynaLivePlayer/pkg/eventbus"
"fyne.io/fyne/v2"
)
// since 2.6.1, calls to fyne API from other go routine must be wrapped in fyne.Do
func ThreadSafeHandler(fn func(e *eventbus.Event)) func(e *eventbus.Event) {
//return fn
return func(e *eventbus.Event) {
fyne.Do(func() {
fn(e)
})
}
}
func RunInFyneThread(fn func()) {
//fn()
fyne.Do(fn)
}

55
gui/gutil/resize.go Normal file
View File

@@ -0,0 +1,55 @@
package gutil
import (
"bytes"
"errors"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"github.com/AynaLivePlayer/miaosic"
"github.com/go-resty/resty/v2"
"github.com/nfnt/resize"
"image"
"image/png"
)
func ResizeImage(resource fyne.Resource, width int, height int) fyne.Resource {
data := resource.Content()
img, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
return resource
}
img = resize.Thumbnail(uint(width), uint(height), img, resize.Lanczos3)
buf := bytes.NewBuffer([]byte{})
err = png.Encode(buf, img)
if err != nil {
return resource
}
return fyne.NewStaticResource(resource.Name(), buf.Bytes())
}
func NewImageFromPlayerPicture(picture miaosic.Picture) (*canvas.Image, error) {
var img *canvas.Image
if picture.Data != nil {
img = canvas.NewImageFromReader(bytes.NewReader(picture.Data), "cover")
// return an error when img is nil
if img == nil {
return nil, errors.New("fail to read image")
}
} else {
get, err := resty.New().R().Get(picture.Url)
if err != nil {
return nil, err
}
img = canvas.NewImageFromReader(bytes.NewReader(get.Body()), "cover")
// NewImageFromURI will return an image with empty resource and file
if img == nil {
return nil, errors.New("fail to download image")
}
}
if img.Resource == nil {
return nil, errors.New("fail to read image")
}
// compress image, so it won't be too large
img.Resource = ResizeImage(img.Resource, 128, 128)
return img, nil
}

18
gui/handler.go Normal file
View File

@@ -0,0 +1,18 @@
package gui
import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/global"
"AynaLivePlayer/pkg/eventbus"
)
func registerHandlers() {
global.EventBus.Subscribe("", events.GUISetPlayerWindowOpenCmd, "gui.player.videoplayer.handleopen", func(event *eventbus.Event) {
data := event.Data.(events.GUISetPlayerWindowOpenCmdEvent)
if data.SetOpen {
playerWindow.Close()
} else {
showPlayerWindow()
}
})
}

Some files were not shown because too many files have changed in this diff Show More