mirror of
https://github.com/AynaLivePlayer/AynaLivePlayer.git
synced 2025-12-07 19:02:50 +08:00
Compare commits
296 Commits
v0.9.9
...
8674e936e2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8674e936e2 | ||
|
|
0bea12d0b3 | ||
|
|
8c0a3cf224 | ||
|
|
8d6ec36609 | ||
|
|
68915b4f7e | ||
|
|
c7fb3e76f9 | ||
|
|
437b4a0f15 | ||
|
|
d1e77e35a4 | ||
|
|
d06ee8f61e | ||
|
|
135c022cec | ||
|
|
3c8c8f3834 | ||
|
|
f070ee3f47 | ||
|
|
f59aebd2f8 | ||
|
|
650da87f64 | ||
|
|
2838a02c83 | ||
|
|
5c508b9664 | ||
|
|
7c3f8587f6 | ||
|
|
918e2e81b3 | ||
|
|
a81eb4a131 | ||
|
|
f13577f890 | ||
|
|
82662c308a | ||
|
|
f6f306edc3 | ||
|
|
a26b58b083 | ||
|
|
093c10092b | ||
|
|
4f53f870f3 | ||
|
|
5a699a1e2e | ||
|
|
3aebdb00f9 | ||
|
|
575e1863fd | ||
|
|
1e538646f4 | ||
|
|
5c016ae145 | ||
|
|
162779f25e | ||
|
|
d2c89b031f | ||
|
|
ccbe2117e0 | ||
|
|
466e4a761e | ||
|
|
07d0a5debc | ||
|
|
7e680bc6bf | ||
|
|
45878dfe99 | ||
|
|
b136a374c4 | ||
|
|
6bf17a0dcb | ||
|
|
da4ae97923 | ||
|
|
2bab6044bf | ||
|
|
2cf80698d9 | ||
|
|
4ac75dd882 | ||
|
|
6af984cfbb | ||
|
|
82ced0b9a9 | ||
|
|
d11a5a5f76 | ||
|
|
df771ed9f8 | ||
|
|
3bd8945247 | ||
|
|
c31342c708 | ||
|
|
46ea1968b6 | ||
|
|
d4f0c3438c | ||
|
|
1a0e053377 | ||
|
|
7b87efb076 | ||
|
|
3f5623f117 | ||
|
|
261166e67d | ||
|
|
dc727935a8 | ||
|
|
68a172aa4f | ||
|
|
bab3a14d2f | ||
|
|
ee99bd939e | ||
|
|
0934bd2d45 | ||
|
|
7de62ef6ba | ||
|
|
af6ef96754 | ||
|
|
09bacbf7da | ||
|
|
21fe844b5f | ||
|
|
72bdef6a91 | ||
|
|
c554f1effc | ||
|
|
8244380a6a | ||
|
|
0bbb9f378f | ||
|
|
eafb074398 | ||
|
|
7661e966be | ||
|
|
06df1a1cc4 | ||
|
|
5b4104664e | ||
|
|
2b99f7b23f | ||
|
|
3b58f6f972 | ||
|
|
374be8ef03 | ||
|
|
f4aa9b4ac9 | ||
|
|
1fa82c2a01 | ||
|
|
cdc3c5d118 | ||
|
|
c7b70740f6 | ||
|
|
dd71c3b9ba | ||
|
|
45e4c15b8d | ||
|
|
da81d43584 | ||
|
|
82df68fa8a | ||
|
|
f897dd0800 | ||
|
|
0ea07dc295 | ||
|
|
edf3b679cc | ||
|
|
041b678859 | ||
|
|
1b58c7bc08 | ||
|
|
826e868916 | ||
|
|
32eab60cad | ||
|
|
83fbefaf68 | ||
|
|
f62e9ea43a | ||
|
|
3456bfcb8c | ||
|
|
df614a16f6 | ||
|
|
8a96506703 | ||
|
|
53a6ad4aed | ||
|
|
387e96e27e | ||
|
|
8da41f81e1 | ||
|
|
42c80f3ea8 | ||
|
|
e311c40f5e | ||
|
|
cbba7dd1b3 | ||
|
|
e598ed676c | ||
|
|
62a122a9e5 | ||
|
|
98028885b8 | ||
|
|
64c7780b60 | ||
|
|
d40ef5d79d | ||
|
|
32e688bb91 | ||
|
|
df501be65a | ||
|
|
fdf12bde26 | ||
|
|
cbde61d99b | ||
|
|
2b300af7b7 | ||
|
|
1bbaf478bb | ||
|
|
72589a5eaa | ||
|
|
0424513d86 | ||
|
|
f7b6a5eafb | ||
|
|
1c5e85010b | ||
|
|
d9581c0bab | ||
|
|
8114e69b9f | ||
|
|
b178f729a2 | ||
|
|
3d5286b319 | ||
|
|
5afbe695f2 | ||
|
|
f36f56cd80 | ||
|
|
97faa1a538 | ||
|
|
93e8d4d846 | ||
|
|
4e3dd6400f | ||
|
|
27a2f1f3a8 | ||
|
|
bc85f86f98 | ||
|
|
c1940c0a98 | ||
|
|
4e6659935e | ||
|
|
9941fc6993 | ||
|
|
e7c499ca7f | ||
|
|
e61f096bb2 | ||
|
|
c1c230a5f0 | ||
|
|
a2420db1d9 | ||
|
|
0e4a429a19 | ||
|
|
d69f953508 | ||
|
|
04406bf006 | ||
|
|
b5c2a0ce96 | ||
|
|
13ffb609a9 | ||
|
|
2e165d5d0e | ||
|
|
0fe77febdf | ||
|
|
539e8eabe3 | ||
|
|
7960299f09 | ||
|
|
e7f7ddfe4a | ||
|
|
23d6944a52 | ||
|
|
ef994defb9 | ||
|
|
2056bd310a | ||
|
|
46c2e2710e | ||
|
|
156901f14c | ||
|
|
9f8b103be1 | ||
|
|
25bc0f01ca | ||
|
|
c5d3fb407b | ||
|
|
5d507598e6 | ||
|
|
5480ebdd86 | ||
|
|
0e3527c166 | ||
|
|
48e5d2f516 | ||
|
|
b7f10431d6 | ||
|
|
0c976acf80 | ||
|
|
a06412e833 | ||
|
|
7e089913be | ||
|
|
61596a5b79 | ||
|
|
e44285841f | ||
|
|
93b3bbbb1f | ||
|
|
29509af04e | ||
|
|
6073719d69 | ||
|
|
c9d84b1134 | ||
|
|
4a76cbdb8b | ||
|
|
4cf79a0ab9 | ||
|
|
56b484257b | ||
|
|
e8953f1e56 | ||
|
|
1aace54b92 | ||
|
|
0e64d1dc91 | ||
|
|
ea05fcd902 | ||
|
|
afa2729282 | ||
|
|
36cdfd0824 | ||
|
|
ce042bad0a | ||
|
|
3959eac396 | ||
|
|
2ab27b1b57 | ||
|
|
7316818093 | ||
|
|
fb43dcab5d | ||
|
|
fa7448e2c5 | ||
|
|
5d002becc4 | ||
|
|
a2d674666a | ||
|
|
a6cdf3c903 | ||
|
|
a892ec3043 | ||
|
|
2bc19e2e0c | ||
|
|
552f115a33 | ||
|
|
92376bdd12 | ||
|
|
9142d74f19 | ||
|
|
65aba16f44 | ||
|
|
37f0dadd8b | ||
|
|
c57b7c992f | ||
|
|
e47b48bb42 | ||
|
|
deec6157b9 | ||
|
|
7634bd5864 | ||
|
|
50d3172816 | ||
|
|
e466e21bc8 | ||
|
|
7a57a1dcc0 | ||
|
|
627e5ff9bc | ||
|
|
ce1322fbde | ||
|
|
8db6f49c7b | ||
|
|
161920686f | ||
|
|
6b595e453b | ||
|
|
36df238ac1 | ||
|
|
92f36057a4 | ||
|
|
b175efa6e3 | ||
|
|
4aa5071699 | ||
|
|
318e062198 | ||
|
|
16c9d1c10e | ||
|
|
23d59417d5 | ||
|
|
c6a8a2b272 | ||
|
|
61342ebfa2 | ||
|
|
1df1f3a609 | ||
|
|
891f3d2879 | ||
|
|
0dbb9976d7 | ||
|
|
eabd9fcea3 | ||
|
|
ec110cca9c | ||
|
|
f6fd6d362b | ||
|
|
6ec3987b50 | ||
|
|
812dee0145 | ||
|
|
16f0aa34a2 | ||
|
|
03862237b4 | ||
|
|
05322ab0b3 | ||
|
|
f97c460c46 | ||
|
|
8d24ac8cba | ||
|
|
f19babe30a | ||
|
|
9b55e42811 | ||
|
|
963dedbe65 | ||
|
|
b031bdd3df | ||
|
|
aeca816774 | ||
|
|
9b6f681d4a | ||
|
|
46ea45580c | ||
|
|
da96f711ae | ||
|
|
399f09ba9f | ||
|
|
2326ef6955 | ||
|
|
23db890d47 | ||
|
|
5145680a04 | ||
|
|
d12a0155b0 | ||
|
|
ac8633b4a7 | ||
|
|
9b9895e654 | ||
|
|
f54e01f7ea | ||
|
|
f24b3e73fb | ||
|
|
68c7c591ff | ||
|
|
95a0a97264 | ||
|
|
e5076667db | ||
|
|
726ac8b449 | ||
|
|
88066bd3b9 | ||
|
|
d514f96c28 | ||
|
|
9a277482b4 | ||
|
|
39db106a74 | ||
|
|
0e5140f907 | ||
|
|
3786355997 | ||
|
|
a5656b8ef0 | ||
|
|
884a5afcb5 | ||
|
|
bec84790fd | ||
|
|
e7775019f7 | ||
|
|
3fb7941433 | ||
|
|
b56a1073a6 | ||
|
|
eba37c04bc | ||
|
|
de3d2f6c66 | ||
|
|
a8091247ec | ||
|
|
9c0b711aa0 | ||
|
|
58d7ebd43d | ||
|
|
ee775dee8d | ||
|
|
5d27040c8b | ||
|
|
2169817afa | ||
|
|
e9149474de | ||
|
|
5cc5948a85 | ||
|
|
92ea73ff5a | ||
|
|
e567b5b47b | ||
|
|
802961576c | ||
|
|
0ad0296e51 | ||
|
|
f484b28d0e | ||
|
|
e2fa2bdb8e | ||
|
|
e01d067cbc | ||
|
|
44203f9887 | ||
|
|
ccb3ccbc00 | ||
|
|
d64cac9b1b | ||
|
|
6ef4ba3fe8 | ||
|
|
d30db8a95a | ||
|
|
30a2f9ebea | ||
|
|
6ddcb507f5 | ||
|
|
fbf8c7f149 | ||
|
|
139f331a14 | ||
|
|
c9cd78b0eb | ||
|
|
e7325b6383 | ||
|
|
fd872c1f5b | ||
|
|
119862a023 | ||
|
|
d52f97429d | ||
|
|
084050fcf7 | ||
|
|
f926f15606 | ||
|
|
8d73a3c284 | ||
|
|
c993684497 | ||
|
|
0ae52e349d | ||
|
|
cb092366f3 | ||
|
|
18df9ff64c |
124
.github/workflows/build.yml
vendored
Normal file
124
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,124 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build-windows:
|
||||
runs-on: windows-2022
|
||||
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.24'
|
||||
|
||||
- name: Install Go Dependencies
|
||||
run: |
|
||||
go mod tidy
|
||||
go install fyne.io/tools/cmd/fyne@latest
|
||||
|
||||
- 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.24'
|
||||
|
||||
- name: Install Go Dependencies
|
||||
run: |
|
||||
go mod tidy
|
||||
go install fyne.io/tools/cmd/fyne@latest
|
||||
|
||||
- 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
11
.gitignore
vendored
@@ -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
6
.gitmodules
vendored
Normal 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
|
||||
93
Makefile
93
Makefile
@@ -1,65 +1,58 @@
|
||||
NAME = AynaLivePlayer
|
||||
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
|
||||
|
||||
ifeq ($(OS), Windows_NT)
|
||||
EXECUTABLE=$(NAME).exe
|
||||
SCRIPTPATH = .\assets\scripts\windows
|
||||
else
|
||||
EXECUTABLE=$(NAME)
|
||||
SCRIPTPATH = ./assets/scripts/linux
|
||||
endif
|
||||
|
||||
gui: bundle
|
||||
go build -o $(EXECUTABLE) -ldflags -H=windowsgui main.go
|
||||
|
||||
run: bundle
|
||||
go run main.go
|
||||
|
||||
clear:
|
||||
$(RM) config.ini log.txt playlists.txt liverooms.json
|
||||
|
||||
bundle:
|
||||
fyne bundle --name resImageEmpty --package resource ./assets/empty.png > ./resource/bundle.go
|
||||
fyne bundle --append --name resImageIcon --package resource ./assets/icon.jpg >> ./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
|
||||
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
|
||||
|
||||
release: gui
|
||||
-mkdir release
|
||||
ifeq ($(OS), Windows_NT)
|
||||
COPY .\$(EXECUTABLE) .\release\$(EXECUTABLE)
|
||||
COPY .\webtemplates.json .\release\webtemplates.json
|
||||
mkdir .\release\assets
|
||||
COPY .\assets\mpv-2.dll .\release\mpv-2.dll
|
||||
COPY .\assets\translation.json .\release\assets\translation.json
|
||||
COPY LICENSE.md .\release\LICENSE.md
|
||||
XCOPY .\assets\scripts\windows\* .\release\ /k /i /y /q
|
||||
XCOPY .\assets\webinfo .\release\assets\webinfo /s /e /i /y /q
|
||||
XCOPY .\music .\release\music /s /e /i /y /q
|
||||
XCOPY .\template .\release\template /s /e /i /y /q
|
||||
else
|
||||
cp ./$(EXECUTABLE) ./release/$(EXECUTABLE)
|
||||
cp ./webtemplates.json ./release/webtemplates.json
|
||||
cp ./assets/translation.json ./release/assets/translation.json
|
||||
mkdir ./release/assets
|
||||
cp LICENSE.md ./release/LICENSE.md
|
||||
cp ./assets/scripts/linux/* ./release/
|
||||
cp -r ./assets/webinfo ./release/assest/webinfo
|
||||
cp -r ./music ./release/music
|
||||
cp -r ./template ./release/template
|
||||
endif
|
||||
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:
|
||||
$(RM) $(EXECUTABLE) config.ini log.txt playlists.txt liverooms.json
|
||||
$(RRM) release
|
||||
|
||||
.PHONY: ${EXECUTABLE}
|
||||
$(RRM) ./release
|
||||
39
README.md
39
README.md
@@ -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
|
||||
```
|
||||
@@ -1,24 +0,0 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/adapters/liveclient"
|
||||
"AynaLivePlayer/adapters/logger"
|
||||
"AynaLivePlayer/adapters/player"
|
||||
"AynaLivePlayer/common/event"
|
||||
"AynaLivePlayer/core/adapter"
|
||||
)
|
||||
|
||||
var Logger = &logger.LoggerFactory{}
|
||||
|
||||
var LiveClient = &liveclient.LiveClientFactory{
|
||||
LiveClients: map[string]adapter.LiveClientCtor{
|
||||
"bilibili": liveclient.BilibiliCtor,
|
||||
},
|
||||
EventManager: event.MainManager,
|
||||
Logger: &logger.EmptyLogger{},
|
||||
}
|
||||
|
||||
var Player = &player.PlayerFactory{
|
||||
EventManager: event.MainManager,
|
||||
Logger: &logger.EmptyLogger{},
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package liveclient
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/common/event"
|
||||
"AynaLivePlayer/core/adapter"
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/core/model"
|
||||
"errors"
|
||||
"github.com/aynakeya/blivedm"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Bilibili struct {
|
||||
client *blivedm.BLiveWsClient
|
||||
eventManager *event.Manager
|
||||
roomName string
|
||||
status bool
|
||||
log adapter.ILogger
|
||||
}
|
||||
|
||||
func BilibiliCtor(id string, em *event.Manager, log adapter.ILogger) (adapter.LiveClient, error) {
|
||||
room, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return nil, errors.New("room id for bilibili should be a integer")
|
||||
}
|
||||
return NewBilibili(room, em, log), nil
|
||||
}
|
||||
|
||||
func NewBilibili(roomId int, em *event.Manager, log adapter.ILogger) adapter.LiveClient {
|
||||
cl := &Bilibili{
|
||||
client: &blivedm.BLiveWsClient{ShortId: roomId, Account: blivedm.DanmuAccount{UID: 0}, HearbeatInterval: 10 * time.Second},
|
||||
eventManager: em,
|
||||
roomName: "",
|
||||
log: log,
|
||||
}
|
||||
cl.client.OnDisconnect = func(client *blivedm.BLiveWsClient) {
|
||||
cl.log.Warn("[Bilibili LiveChatSDK] disconnect from websocket connection, maybe try reconnect")
|
||||
cl.status = false
|
||||
cl.eventManager.CallA(events.LiveRoomStatusChange, events.StatusChangeEvent{Connected: false, Client: cl})
|
||||
}
|
||||
cl.client.RegHandler(blivedm.CmdDanmaku, cl.handleMsg)
|
||||
return cl
|
||||
}
|
||||
|
||||
func (b *Bilibili) ClientName() string {
|
||||
return "bilibili"
|
||||
}
|
||||
|
||||
func (b *Bilibili) RoomName() string {
|
||||
return b.roomName
|
||||
}
|
||||
|
||||
func (b *Bilibili) Status() bool {
|
||||
return b.status
|
||||
}
|
||||
|
||||
func (b *Bilibili) EventManager() *event.Manager {
|
||||
return b.eventManager
|
||||
}
|
||||
|
||||
func (b *Bilibili) Connect() bool {
|
||||
if b.status {
|
||||
return true
|
||||
}
|
||||
b.log.Info("[Bilibili LiveChatSDK] Trying Connect Danmu Server")
|
||||
if b.client.InitRoom() && b.client.ConnectDanmuServer() {
|
||||
b.roomName = b.client.RoomInfo.Title
|
||||
b.status = true
|
||||
b.eventManager.CallA(events.LiveRoomStatusChange, events.StatusChangeEvent{Connected: true, Client: b})
|
||||
b.log.Info("[Bilibili LiveChatSDK] Connect Success")
|
||||
return true
|
||||
}
|
||||
b.log.Info("[Bilibili LiveChatSDK] Connect Failed")
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *Bilibili) Disconnect() bool {
|
||||
b.log.Info("[Bilibili LiveChatSDK] Disconnect from danmu server")
|
||||
if b.client == nil {
|
||||
return true
|
||||
}
|
||||
b.client.Disconnect()
|
||||
b.eventManager.CallA(events.LiveRoomStatusChange, events.StatusChangeEvent{Connected: false, Client: b})
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *Bilibili) handleMsg(context *blivedm.Context) {
|
||||
msg, ok := context.ToDanmakuMessage()
|
||||
if !ok {
|
||||
b.log.Warn("[Bilibili LiveChatSDK] handle message failed, can't convert context to danmu message")
|
||||
return
|
||||
}
|
||||
dmsg := model.DanmuMessage{
|
||||
User: model.DanmuUser{
|
||||
Uid: strconv.FormatInt(msg.Uid, 10),
|
||||
Username: msg.Uname,
|
||||
Medal: model.UserMedal{
|
||||
Name: msg.MedalName,
|
||||
Level: int(msg.MedalLevel),
|
||||
},
|
||||
Admin: msg.Admin,
|
||||
Privilege: int(msg.PrivilegeType),
|
||||
},
|
||||
Message: msg.Msg,
|
||||
}
|
||||
b.log.Debug("[Bilibili LiveChatSDK] receive message", dmsg)
|
||||
go func() {
|
||||
b.eventManager.Call(&event.Event{
|
||||
Id: events.LiveRoomMessageReceive,
|
||||
Cancelled: false,
|
||||
Data: &dmsg,
|
||||
})
|
||||
}()
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package liveclient
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/common/event"
|
||||
"AynaLivePlayer/core/adapter"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type LiveClientFactory struct {
|
||||
LiveClients map[string]adapter.LiveClientCtor
|
||||
EventManager *event.Manager
|
||||
Logger adapter.ILogger
|
||||
}
|
||||
|
||||
func (f *LiveClientFactory) GetAllClientNames() []string {
|
||||
names := make([]string, 0)
|
||||
for key, _ := range f.LiveClients {
|
||||
names = append(names, key)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func (f *LiveClientFactory) NewLiveClient(clientName, id string) (adapter.LiveClient, error) {
|
||||
ctor, ok := f.LiveClients[clientName]
|
||||
if !ok {
|
||||
return nil, errors.New("no such client")
|
||||
}
|
||||
return ctor(id, f.EventManager.NewChildManager(), f.Logger)
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/adapter"
|
||||
)
|
||||
|
||||
type EmptyLogger struct {
|
||||
}
|
||||
|
||||
func (e EmptyLogger) Debug(args ...interface{}) {
|
||||
return
|
||||
}
|
||||
|
||||
func (e EmptyLogger) Debugf(format string, args ...interface{}) {
|
||||
return
|
||||
}
|
||||
|
||||
func (e EmptyLogger) Info(args ...interface{}) {
|
||||
return
|
||||
}
|
||||
|
||||
func (e EmptyLogger) Infof(format string, args ...interface{}) {
|
||||
return
|
||||
}
|
||||
|
||||
func (e EmptyLogger) Warn(args ...interface{}) {
|
||||
return
|
||||
}
|
||||
|
||||
func (e EmptyLogger) Warnf(format string, args ...interface{}) {
|
||||
return
|
||||
}
|
||||
|
||||
func (e EmptyLogger) Error(args ...interface{}) {
|
||||
return
|
||||
}
|
||||
|
||||
func (e EmptyLogger) Errorf(format string, args ...interface{}) {
|
||||
return
|
||||
}
|
||||
|
||||
func (e EmptyLogger) WithModule(prefix string) adapter.ILogger {
|
||||
return &EmptyLogger{}
|
||||
}
|
||||
|
||||
func (e EmptyLogger) SetLogLevel(level adapter.LogLevel) {
|
||||
return
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/adapter"
|
||||
)
|
||||
|
||||
type LoggerFactory struct {
|
||||
LiveClients map[string]adapter.LiveClientCtor
|
||||
}
|
||||
|
||||
func (f *LoggerFactory) NewLogrus(filename string, redirectStderr bool) adapter.ILogger {
|
||||
return NewLogrusLogger(filename, redirectStderr)
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/adapter"
|
||||
nested "github.com/antonfisher/nested-logrus-formatter"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/virtuald/go-paniclog"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
type LogrusLogger struct {
|
||||
*logrus.Entry
|
||||
module string
|
||||
}
|
||||
|
||||
func (l *LogrusLogger) SetLogLevel(level adapter.LogLevel) {
|
||||
switch level {
|
||||
case adapter.LogLevelDebug:
|
||||
l.Logger.SetLevel(logrus.DebugLevel)
|
||||
case adapter.LogLevelInfo:
|
||||
l.Logger.SetLevel(logrus.InfoLevel)
|
||||
case adapter.LogLevelWarn:
|
||||
l.Logger.SetLevel(logrus.WarnLevel)
|
||||
case adapter.LogLevelError:
|
||||
l.Logger.SetLevel(logrus.ErrorLevel)
|
||||
default:
|
||||
l.Logger.SetLevel(logrus.InfoLevel)
|
||||
}
|
||||
}
|
||||
|
||||
func NewLogrusLogger(fileName string, redirectStderr bool) *LogrusLogger {
|
||||
l := logrus.New()
|
||||
l.SetFormatter(
|
||||
&nested.Formatter{
|
||||
FieldsOrder: []string{"Module"},
|
||||
HideKeys: true,
|
||||
NoColors: true,
|
||||
})
|
||||
var file *os.File
|
||||
var err error
|
||||
if fileName != "" {
|
||||
file, err = os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err == nil {
|
||||
l.Out = io.MultiWriter(file, os.Stdout)
|
||||
} else {
|
||||
l.Info("Failed to log to file, using default stdout")
|
||||
}
|
||||
}
|
||||
if redirectStderr && file != nil {
|
||||
l.Info("panic/stderr redirect to log file")
|
||||
if _, err = paniclog.RedirectStderr(file); err != nil {
|
||||
l.Infof("Failed to redirect stderr to to file: %s", err)
|
||||
}
|
||||
}
|
||||
return &LogrusLogger{
|
||||
Entry: logrus.NewEntry(l),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LogrusLogger) WithModule(prefix string) adapter.ILogger {
|
||||
return &LogrusLogger{
|
||||
Entry: l.Entry.WithField("Module", prefix),
|
||||
module: prefix,
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/common/event"
|
||||
"AynaLivePlayer/core/adapter"
|
||||
)
|
||||
|
||||
type PlayerFactory struct {
|
||||
EventManager *event.Manager
|
||||
Logger adapter.ILogger
|
||||
}
|
||||
|
||||
func (f *PlayerFactory) NewMPV() adapter.IPlayer {
|
||||
return NewMpvPlayer(f.EventManager, f.Logger)
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/common/event"
|
||||
"AynaLivePlayer/common/util"
|
||||
"AynaLivePlayer/core/adapter"
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/core/model"
|
||||
"fmt"
|
||||
"github.com/aynakeya/go-mpv"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
var mpvPropertyMap = map[model.PlayerProperty]string{
|
||||
model.PlayerPropDuration: "duration",
|
||||
model.PlayerPropTimePos: "time-pos",
|
||||
model.PlayerPropIdleActive: "idle-active",
|
||||
model.PlayerPropPercentPos: "percent-pos",
|
||||
model.PlayerPropPause: "pause",
|
||||
model.PlayerPropVolume: "volume",
|
||||
}
|
||||
|
||||
var mpvPropertyMapInv = map[string]model.PlayerProperty{
|
||||
"duration": model.PlayerPropDuration,
|
||||
"time-pos": model.PlayerPropTimePos,
|
||||
"idle-active": model.PlayerPropIdleActive,
|
||||
"percent-pos": model.PlayerPropPercentPos,
|
||||
"pause": model.PlayerPropPause,
|
||||
"volume": model.PlayerPropVolume,
|
||||
}
|
||||
|
||||
type MpvPlayer struct {
|
||||
running bool
|
||||
libmpv *mpv.Mpv
|
||||
Playing *model.Media
|
||||
propertyWatchedFlag map[model.PlayerProperty]int
|
||||
eventManager *event.Manager
|
||||
log adapter.ILogger
|
||||
}
|
||||
|
||||
func NewMpvPlayer(em *event.Manager, log adapter.ILogger) adapter.IPlayer {
|
||||
player := &MpvPlayer{
|
||||
running: true,
|
||||
libmpv: mpv.Create(),
|
||||
propertyWatchedFlag: make(map[model.PlayerProperty]int),
|
||||
eventManager: em,
|
||||
log: log,
|
||||
}
|
||||
err := player.libmpv.Initialize()
|
||||
if err != nil {
|
||||
player.log.Error("[MPV Player] initialize libmpv failed")
|
||||
return nil
|
||||
}
|
||||
_ = player.libmpv.SetOptionString("vo", "null")
|
||||
player.log.Info("[MPV Player] initialize libmpv success")
|
||||
_ = player.ObserveProperty(model.PlayerPropIdleActive, "player.setplaying", func(evnt *event.Event) {
|
||||
isIdle := evnt.Data.(events.PlayerPropertyUpdateEvent).Value.(bool)
|
||||
if isIdle {
|
||||
player.Playing = nil
|
||||
}
|
||||
})
|
||||
player.Start()
|
||||
return player
|
||||
}
|
||||
|
||||
func (p *MpvPlayer) Start() {
|
||||
p.log.Info("[MPV Player] starting mpv player")
|
||||
go func() {
|
||||
for p.running {
|
||||
e := p.libmpv.WaitEvent(1)
|
||||
if e == nil {
|
||||
p.log.Warn("[MPV Player] event loop got nil event")
|
||||
}
|
||||
//p.log.Trace("[MPV Player] new event", e)
|
||||
if e.EventId == mpv.EVENT_PROPERTY_CHANGE {
|
||||
eventProperty := e.Property()
|
||||
property, ok := mpvPropertyMapInv[eventProperty.Name]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
var value interface{} = nil
|
||||
if eventProperty.Data != nil {
|
||||
value = eventProperty.Data.(mpv.Node).Value
|
||||
}
|
||||
p.eventManager.CallA(
|
||||
events.EventPlayerPropertyUpdate(property),
|
||||
events.PlayerPropertyUpdateEvent{
|
||||
Property: property,
|
||||
Value: value,
|
||||
})
|
||||
|
||||
}
|
||||
if e.EventId == mpv.EVENT_SHUTDOWN {
|
||||
p.log.Info("[MPV Player] libmpv shutdown")
|
||||
p.Stop()
|
||||
}
|
||||
}
|
||||
}()
|
||||
return
|
||||
}
|
||||
|
||||
func (p *MpvPlayer) Stop() {
|
||||
p.log.Info("[MPV Player] stopping mpv player")
|
||||
p.running = false
|
||||
p.libmpv.TerminateDestroy()
|
||||
}
|
||||
|
||||
func (p *MpvPlayer) GetPlaying() *model.Media {
|
||||
return p.Playing
|
||||
}
|
||||
|
||||
func (p *MpvPlayer) SetWindowHandle(handle uintptr) error {
|
||||
p.log.Infof("[MPV Player] set window handle %d", handle)
|
||||
_ = p.libmpv.SetOptionString("wid", fmt.Sprintf("%d", handle))
|
||||
return p.libmpv.SetOptionString("vo", "gpu")
|
||||
}
|
||||
|
||||
func (p *MpvPlayer) Play(media *model.Media) error {
|
||||
p.log.Infof("[MPV Player] Play media %s", media.Url)
|
||||
if val, ok := media.Header["User-Agent"]; ok {
|
||||
p.log.Debug("[MPV PlayControl] set user-agent for mpv player")
|
||||
err := p.libmpv.SetPropertyString("user-agent", val)
|
||||
if err != nil {
|
||||
p.log.Warn("[MPV PlayControl] set player user-agent failed", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if val, ok := media.Header["Referer"]; ok {
|
||||
p.log.Debug("[MPV PlayControl] set referrer for mpv player")
|
||||
err := p.libmpv.SetPropertyString("referrer", val)
|
||||
if err != nil {
|
||||
p.log.Warn("[MPV PlayControl] set player referrer failed", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
p.log.Debugf("mpv command load file %s %s", media.Title, media.Url)
|
||||
if err := p.libmpv.Command([]string{"loadfile", media.Url}); err != nil {
|
||||
p.log.Warn("[MPV PlayControl] mpv load media failed", media)
|
||||
return err
|
||||
}
|
||||
p.Playing = media
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MpvPlayer) IsPaused() bool {
|
||||
property, err := p.libmpv.GetProperty("pause", mpv.FORMAT_FLAG)
|
||||
if err != nil {
|
||||
p.log.Warn("[MPV PlayControl] get property pause failed", err)
|
||||
return false
|
||||
}
|
||||
return property.(bool)
|
||||
}
|
||||
|
||||
func (p *MpvPlayer) Pause() error {
|
||||
p.log.Debugf("[MPV Player] pause")
|
||||
return p.libmpv.SetProperty("pause", mpv.FORMAT_FLAG, true)
|
||||
}
|
||||
|
||||
func (p *MpvPlayer) Unpause() error {
|
||||
p.log.Debugf("[MPV Player] unpause")
|
||||
return p.libmpv.SetProperty("pause", mpv.FORMAT_FLAG, false)
|
||||
}
|
||||
|
||||
// SetVolume set mpv volume, from 0.0 - 100.0
|
||||
func (p *MpvPlayer) SetVolume(volume float64) error {
|
||||
p.log.Debugf("[MPV Player] set volume to %f", volume)
|
||||
return p.libmpv.SetProperty("volume", mpv.FORMAT_DOUBLE, volume)
|
||||
}
|
||||
|
||||
func (p *MpvPlayer) IsIdle() bool {
|
||||
property, err := p.libmpv.GetProperty("idle-active", mpv.FORMAT_FLAG)
|
||||
if err != nil {
|
||||
p.log.Warn("[MPV Player] get property idle-active failed", err)
|
||||
return false
|
||||
}
|
||||
return property.(bool)
|
||||
}
|
||||
|
||||
// Seek change position for current file
|
||||
// absolute = true : position is the time in second
|
||||
// absolute = false: position is in percentage eg 0.1 0.2
|
||||
func (p *MpvPlayer) Seek(position float64, absolute bool) error {
|
||||
p.log.Debugf("[MPV Player] seek to %f (absolute=%t)", position, absolute)
|
||||
if absolute {
|
||||
return p.libmpv.SetProperty("time-pos", mpv.FORMAT_DOUBLE, position)
|
||||
} else {
|
||||
return p.libmpv.SetProperty("percent-pos", mpv.FORMAT_DOUBLE, position)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *MpvPlayer) ObserveProperty(property model.PlayerProperty, name string, handler event.HandlerFunc) error {
|
||||
p.log.Debugf("[MPV Player] add property observer for mpv")
|
||||
p.eventManager.RegisterA(
|
||||
events.EventPlayerPropertyUpdate(property),
|
||||
name, handler)
|
||||
if _, ok := p.propertyWatchedFlag[property]; !ok {
|
||||
p.propertyWatchedFlag[property] = 1
|
||||
return p.libmpv.ObserveProperty(util.Hash64(mpvPropertyMap[property]), mpvPropertyMap[property], mpv.FORMAT_NODE)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAudioDeviceList get output device for mpv
|
||||
// return format is []AudioDevice
|
||||
func (p *MpvPlayer) GetAudioDeviceList() ([]model.AudioDevice, error) {
|
||||
p.log.Debugf("[MPV Player] getting audio device list for mpv")
|
||||
property, err := p.libmpv.GetProperty("audio-device-list", mpv.FORMAT_STRING)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dl := make([]model.AudioDevice, 0)
|
||||
gjson.Parse(property.(string)).ForEach(func(key, value gjson.Result) bool {
|
||||
dl = append(dl, model.AudioDevice{
|
||||
Name: value.Get("name").String(),
|
||||
Description: value.Get("description").String(),
|
||||
})
|
||||
return true
|
||||
})
|
||||
return dl, nil
|
||||
}
|
||||
|
||||
func (p *MpvPlayer) SetAudioDevice(device string) error {
|
||||
p.log.Debugf("[MPV Player] set audio device %s for mpv", device)
|
||||
return p.libmpv.SetPropertyString("audio-device", device)
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/adapters/logger"
|
||||
"AynaLivePlayer/common/event"
|
||||
"AynaLivePlayer/core/model"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPlayer(t *testing.T) {
|
||||
player := NewMpvPlayer(event.MainManager, &logger.EmptyLogger{})
|
||||
player.Start()
|
||||
defer player.Stop()
|
||||
|
||||
player.ObserveProperty("time-pos", "testplayer.timepos", func(evnt *event.Event) {
|
||||
fmt.Println(1, evnt.Data)
|
||||
})
|
||||
player.ObserveProperty("percent-pos", "testplayer.percentpos", func(evnt *event.Event) {
|
||||
fmt.Println(2, evnt.Data)
|
||||
})
|
||||
player.Play(&model.Media{
|
||||
Url: "https://ia600809.us.archive.org/19/items/VillagePeopleYMCAOFFICIALMusicVideo1978/Village%20People%20-%20YMCA%20OFFICIAL%20Music%20Video%201978.mp4",
|
||||
})
|
||||
time.Sleep(time.Second * 15)
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/adapter"
|
||||
"AynaLivePlayer/core/model"
|
||||
"fmt"
|
||||
"github.com/tidwall/gjson"
|
||||
"net/url"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type Bilibili struct {
|
||||
InfoApi string
|
||||
FileApi string
|
||||
SearchApi string
|
||||
IdRegex0 *regexp.Regexp
|
||||
IdRegex1 *regexp.Regexp
|
||||
}
|
||||
|
||||
func NewBilibili(config adapter.MediaProviderConfig) adapter.MediaProvider {
|
||||
return &Bilibili{
|
||||
InfoApi: "https://www.bilibili.com/audio/music-service-c/web/song/info?sid=%s",
|
||||
FileApi: "https://api.bilibili.com/audio/music-service-c/url?device=phone&mid=8047632&mobi_app=iphone&platform=ios&privilege=2&songid=%s&quality=2",
|
||||
SearchApi: "https://api.bilibili.com/audio/music-service-c/s?search_type=music&keyword=%s&page=1&pagesize=100",
|
||||
IdRegex0: regexp.MustCompile("^[0-9]+"),
|
||||
IdRegex1: regexp.MustCompile("^au[0-9]+"),
|
||||
}
|
||||
}
|
||||
|
||||
func _newBilibili() *Bilibili {
|
||||
return &Bilibili{
|
||||
InfoApi: "https://www.bilibili.com/audio/music-service-c/web/song/info?sid=%s",
|
||||
FileApi: "https://api.bilibili.com/audio/music-service-c/url?device=phone&mid=8047632&mobi_app=iphone&platform=ios&privilege=2&songid=%s&quality=2",
|
||||
SearchApi: "https://api.bilibili.com/audio/music-service-c/s?search_type=music&keyword=%s&page=1&pagesize=100",
|
||||
IdRegex0: regexp.MustCompile("^[0-9]+"),
|
||||
IdRegex1: regexp.MustCompile("^au[0-9]+"),
|
||||
}
|
||||
}
|
||||
|
||||
var BilibiliAPI *Bilibili
|
||||
|
||||
func init() {
|
||||
BilibiliAPI = _newBilibili()
|
||||
Providers[BilibiliAPI.GetName()] = BilibiliAPI
|
||||
}
|
||||
|
||||
func (b *Bilibili) GetName() string {
|
||||
return "bilibili"
|
||||
}
|
||||
|
||||
func (b *Bilibili) MatchMedia(keyword string) *model.Media {
|
||||
if id := b.IdRegex0.FindString(keyword); id != "" {
|
||||
return &model.Media{
|
||||
Meta: model.Meta{
|
||||
Name: b.GetName(),
|
||||
Id: id,
|
||||
},
|
||||
}
|
||||
}
|
||||
if id := b.IdRegex1.FindString(keyword); id != "" {
|
||||
return &model.Media{
|
||||
Meta: model.Meta{
|
||||
Name: b.GetName(),
|
||||
Id: id[2:],
|
||||
},
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bilibili) FormatPlaylistUrl(uri string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (b *Bilibili) GetPlaylist(playlist *model.Meta) ([]*model.Media, error) {
|
||||
return nil, ErrorExternalApi
|
||||
}
|
||||
|
||||
func (b *Bilibili) Search(keyword string) ([]*model.Media, error) {
|
||||
resp := httpGetString(fmt.Sprintf(b.SearchApi, url.QueryEscape(keyword)), map[string]string{
|
||||
"user-agent": "BiliMusic/2.233.3",
|
||||
})
|
||||
if resp == "" {
|
||||
return nil, ErrorExternalApi
|
||||
}
|
||||
result := make([]*model.Media, 0)
|
||||
gjson.Get(resp, "data.result").ForEach(func(key, value gjson.Result) bool {
|
||||
result = append(result, &model.Media{
|
||||
Title: value.Get("title").String(),
|
||||
Cover: model.Picture{Url: value.Get("cover").String()},
|
||||
Artist: value.Get("author").String(),
|
||||
Meta: model.Meta{
|
||||
Name: b.GetName(),
|
||||
Id: value.Get("id").String(),
|
||||
},
|
||||
})
|
||||
return true
|
||||
})
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (b *Bilibili) UpdateMedia(media *model.Media) error {
|
||||
resp := httpGetString(fmt.Sprintf(b.InfoApi, media.Meta.(model.Meta).Id), map[string]string{
|
||||
"user-agent": "BiliMusic/2.233.3",
|
||||
})
|
||||
if resp == "" {
|
||||
return ErrorExternalApi
|
||||
}
|
||||
if gjson.Get(resp, "data.title").String() == "" {
|
||||
return ErrorExternalApi
|
||||
}
|
||||
media.Title = gjson.Get(resp, "data.title").String()
|
||||
media.Cover.Url = gjson.Get(resp, "data.cover").String()
|
||||
media.Artist = gjson.Get(resp, "data.author").String()
|
||||
media.Album = media.Title
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bilibili) UpdateMediaUrl(media *model.Media) error {
|
||||
resp := httpGetString(fmt.Sprintf(b.FileApi, media.Meta.(model.Meta).Id), map[string]string{
|
||||
"user-agent": "BiliMusic/2.233.3",
|
||||
})
|
||||
|
||||
if resp == "" {
|
||||
return ErrorExternalApi
|
||||
}
|
||||
media.Header = map[string]string{
|
||||
"user-agent": "BiliMusic/2.233.3",
|
||||
}
|
||||
uri := gjson.Get(resp, "data.cdns.0").String()
|
||||
if uri == "" {
|
||||
return ErrorExternalApi
|
||||
}
|
||||
media.Url = uri
|
||||
return nil
|
||||
}
|
||||
func (k *Bilibili) UpdateMediaLyric(media *model.Media) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/adapter"
|
||||
"AynaLivePlayer/core/model"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBilibili_Search(t *testing.T) {
|
||||
var api adapter.MediaProvider = BilibiliAPI
|
||||
result, err := api.Search("染 reol")
|
||||
if err != nil {
|
||||
fmt.Println(1, err)
|
||||
return
|
||||
}
|
||||
fmt.Println(result)
|
||||
media := result[0]
|
||||
fmt.Println(*media)
|
||||
err = api.UpdateMediaUrl(media)
|
||||
fmt.Println(err)
|
||||
fmt.Println(media.Url)
|
||||
}
|
||||
|
||||
func TestBilibili_GetMusicMeta(t *testing.T) {
|
||||
var api adapter.MediaProvider = BilibiliAPI
|
||||
|
||||
media := model.Media{
|
||||
Meta: model.Meta{
|
||||
Name: api.GetName(),
|
||||
Id: "1560601",
|
||||
},
|
||||
}
|
||||
err := api.UpdateMedia(&media)
|
||||
fmt.Println(err)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fmt.Println(media)
|
||||
}
|
||||
|
||||
func TestBilibili_GetMusic(t *testing.T) {
|
||||
var api adapter.MediaProvider = BilibiliAPI
|
||||
media := model.Media{
|
||||
Meta: model.Meta{
|
||||
Name: api.GetName(),
|
||||
Id: "1560601",
|
||||
},
|
||||
}
|
||||
err := api.UpdateMedia(&media)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = api.UpdateMediaUrl(&media)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fmt.Println(media)
|
||||
fmt.Println(media.Url)
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/common/util"
|
||||
"AynaLivePlayer/core/adapter"
|
||||
"AynaLivePlayer/core/model"
|
||||
"fmt"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/tidwall/gjson"
|
||||
"net/url"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type BilibiliVideo struct {
|
||||
InfoApi string
|
||||
FileApi string
|
||||
SearchApi string
|
||||
BVRegex *regexp.Regexp
|
||||
IdRegex *regexp.Regexp
|
||||
PageRegex *regexp.Regexp
|
||||
header map[string]string
|
||||
}
|
||||
|
||||
func NewBilibiliVideo(config adapter.MediaProviderConfig) adapter.MediaProvider {
|
||||
return &BilibiliVideo{
|
||||
InfoApi: "https://api.bilibili.com/x/web-interface/view/detail?bvid=%s&aid=&jsonp=jsonp",
|
||||
FileApi: "https://api.bilibili.com/x/player/playurl?type=&otype=json&fourk=1&qn=32&avid=&bvid=%s&cid=%s",
|
||||
SearchApi: "https://api.bilibili.com/x/web-interface/search/type?search_type=video&page=1&keyword=%s",
|
||||
BVRegex: regexp.MustCompile("^BV[0-9A-Za-z]+"),
|
||||
IdRegex: regexp.MustCompile("^BV[0-9A-Za-z]+(\\?p=[0-9]+)?"),
|
||||
PageRegex: regexp.MustCompile("p=[0-9]+"),
|
||||
header: map[string]string{
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
|
||||
"Referer": "https://www.bilibili.com/",
|
||||
"Origin": "https://www.bilibili.com",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func _newBilibiliVideo() *BilibiliVideo {
|
||||
return &BilibiliVideo{
|
||||
InfoApi: "https://api.bilibili.com/x/web-interface/view/detail?bvid=%s&aid=&jsonp=jsonp",
|
||||
FileApi: "https://api.bilibili.com/x/player/playurl?type=&otype=json&fourk=1&qn=32&avid=&bvid=%s&cid=%s",
|
||||
SearchApi: "https://api.bilibili.com/x/web-interface/search/type?search_type=video&page=1&keyword=%s",
|
||||
BVRegex: regexp.MustCompile("^BV[0-9A-Za-z]+"),
|
||||
IdRegex: regexp.MustCompile("^BV[0-9A-Za-z]+(\\?p=[0-9]+)?"),
|
||||
PageRegex: regexp.MustCompile("p=[0-9]+"),
|
||||
header: map[string]string{
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:51.0) Gecko/20100101 Firefox/51.0",
|
||||
"Referer": "https://www.bilibili.com/",
|
||||
"Origin": "https://www.bilibili.com",
|
||||
"Cookie": "buvid3=9A8B3564-BDA9-407F-B45F-D5C40786CA49167618infoc;",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var BilibiliVideoAPI *BilibiliVideo
|
||||
|
||||
func init() {
|
||||
BilibiliVideoAPI = _newBilibiliVideo()
|
||||
Providers[BilibiliVideoAPI.GetName()] = BilibiliVideoAPI
|
||||
}
|
||||
|
||||
func (b *BilibiliVideo) getPage(bv string) int {
|
||||
if page := b.PageRegex.FindString(bv); page != "" {
|
||||
return util.Atoi(page[2:])
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (b *BilibiliVideo) getBv(bv string) string {
|
||||
return b.BVRegex.FindString(bv)
|
||||
}
|
||||
|
||||
func (b *BilibiliVideo) GetName() string {
|
||||
return "bilibili-video"
|
||||
}
|
||||
|
||||
func (b *BilibiliVideo) MatchMedia(keyword string) *model.Media {
|
||||
if id := b.IdRegex.FindString(keyword); id != "" {
|
||||
return &model.Media{
|
||||
Meta: model.Meta{
|
||||
Name: b.GetName(),
|
||||
Id: id,
|
||||
},
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BilibiliVideo) GetPlaylist(playlist *model.Meta) ([]*model.Media, error) {
|
||||
return nil, ErrorExternalApi
|
||||
}
|
||||
|
||||
func (b *BilibiliVideo) FormatPlaylistUrl(uri string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (b *BilibiliVideo) Search(keyword string) ([]*model.Media, error) {
|
||||
resp := httpGetString(fmt.Sprintf(b.SearchApi, url.QueryEscape(keyword)), b.header)
|
||||
if resp == "" {
|
||||
return nil, ErrorExternalApi
|
||||
}
|
||||
jresp := gjson.Parse(resp)
|
||||
if jresp.Get("code").String() != "0" {
|
||||
return nil, ErrorExternalApi
|
||||
}
|
||||
result := make([]*model.Media, 0)
|
||||
r := regexp.MustCompile("</?em[^>]*>")
|
||||
jresp.Get("data.result").ForEach(func(key, value gjson.Result) bool {
|
||||
result = append(result, &model.Media{
|
||||
Title: r.ReplaceAllString(value.Get("title").String(), ""),
|
||||
Cover: model.Picture{Url: "https:" + value.Get("pic").String()},
|
||||
Artist: value.Get("author").String(),
|
||||
Meta: model.Meta{
|
||||
Name: b.GetName(),
|
||||
Id: value.Get("bvid").String(),
|
||||
},
|
||||
})
|
||||
return true
|
||||
})
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (b *BilibiliVideo) UpdateMedia(media *model.Media) error {
|
||||
resp := httpGetString(fmt.Sprintf(b.InfoApi, b.getBv(media.Meta.(model.Meta).Id)), nil)
|
||||
if resp == "" {
|
||||
return ErrorExternalApi
|
||||
}
|
||||
jresp := gjson.Parse(resp)
|
||||
if jresp.Get("data.View.title").String() == "" {
|
||||
return ErrorExternalApi
|
||||
}
|
||||
media.Title = jresp.Get("data.View.title").String()
|
||||
media.Artist = jresp.Get("data.View.owner.name").String()
|
||||
media.Cover.Url = jresp.Get("data.View.pic").String()
|
||||
media.Album = media.Title
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BilibiliVideo) UpdateMediaUrl(media *model.Media) error {
|
||||
resp := httpGetString(fmt.Sprintf(b.InfoApi, b.getBv(media.Meta.(model.Meta).Id)), nil)
|
||||
if resp == "" {
|
||||
return ErrorExternalApi
|
||||
}
|
||||
jresp := gjson.Parse(resp)
|
||||
page := b.getPage(media.Meta.(model.Meta).Id) - 1
|
||||
cid := jresp.Get(fmt.Sprintf("data.View.pages.%d.cid", page)).String()
|
||||
if cid == "" {
|
||||
cid = jresp.Get("data.View.cid").String()
|
||||
}
|
||||
if cid == "" {
|
||||
return ErrorExternalApi
|
||||
}
|
||||
resp = httpGetString(fmt.Sprintf(b.FileApi, b.getBv(media.Meta.(model.Meta).Id), cid), b.header)
|
||||
if resp == "" {
|
||||
return ErrorExternalApi
|
||||
}
|
||||
jresp = gjson.Parse(resp)
|
||||
uri := jresp.Get("data.durl.0.url").String()
|
||||
if uri == "" {
|
||||
return ErrorExternalApi
|
||||
}
|
||||
media.Url = uri
|
||||
header := make(map[string]string)
|
||||
_ = copier.Copy(&header, &b.header)
|
||||
header["Referer"] = fmt.Sprintf("https://www.bilibili.com/video/%s", b.getBv(media.Meta.(model.Meta).Id))
|
||||
media.Header = b.header
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BilibiliVideo) UpdateMediaLyric(media *model.Media) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/adapter"
|
||||
"AynaLivePlayer/core/model"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBV_GetMusicMeta(t *testing.T) {
|
||||
var api adapter.MediaProvider = BilibiliVideoAPI
|
||||
|
||||
media := model.Media{
|
||||
Meta: model.Meta{
|
||||
Name: api.GetName(),
|
||||
Id: "BV1434y1q71P",
|
||||
},
|
||||
}
|
||||
err := api.UpdateMedia(&media)
|
||||
fmt.Println(err)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fmt.Println(media)
|
||||
}
|
||||
|
||||
func TestBV_GetMusic(t *testing.T) {
|
||||
var api adapter.MediaProvider = BilibiliVideoAPI
|
||||
media := model.Media{
|
||||
Meta: model.Meta{
|
||||
Name: api.GetName(),
|
||||
Id: "BV1434y1q71P",
|
||||
},
|
||||
}
|
||||
err := api.UpdateMedia(&media)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = api.UpdateMediaUrl(&media)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
//fmt.Println(media)
|
||||
fmt.Println(media.Url)
|
||||
}
|
||||
|
||||
func TestBV_Regex(t *testing.T) {
|
||||
fmt.Println(regexp.MustCompile("^BV[0-9A-Za-z]+(\\?p=[0-9]+)?").FindString("BV1gA411P7ir?p=3"))
|
||||
}
|
||||
|
||||
func TestBV_GetMusicMeta2(t *testing.T) {
|
||||
var api adapter.MediaProvider = BilibiliVideoAPI
|
||||
|
||||
media := model.Media{
|
||||
Meta: model.Meta{
|
||||
Name: api.GetName(),
|
||||
Id: "BV1gA411P7ir?p=3",
|
||||
},
|
||||
}
|
||||
err := api.UpdateMedia(&media)
|
||||
fmt.Println(err)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fmt.Println(media)
|
||||
}
|
||||
|
||||
func TestBV_GetMusic2(t *testing.T) {
|
||||
var api adapter.MediaProvider = BilibiliVideoAPI
|
||||
media := model.Media{
|
||||
Meta: model.Meta{
|
||||
Name: api.GetName(),
|
||||
Id: "BV1gA411P7ir?p=3",
|
||||
},
|
||||
}
|
||||
err := api.UpdateMedia(&media)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = api.UpdateMediaUrl(&media)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
//fmt.Println(media)
|
||||
fmt.Println(media.Url)
|
||||
}
|
||||
|
||||
func TestBV_Search(t *testing.T) {
|
||||
var api adapter.MediaProvider = BilibiliVideoAPI
|
||||
result, err := api.Search("家有女友")
|
||||
if err != nil {
|
||||
fmt.Println(1, err)
|
||||
return
|
||||
}
|
||||
fmt.Println(len(result))
|
||||
for _, r := range result {
|
||||
fmt.Println(r.Artist)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package provider
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrorExternalApi = errors.New("external api error")
|
||||
ErrorNoSuchProvider = errors.New("not such provider")
|
||||
)
|
||||
@@ -1,29 +0,0 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"github.com/go-resty/resty/v2"
|
||||
"time"
|
||||
)
|
||||
|
||||
func httpGet(url string, header map[string]string) (*resty.Response, error) {
|
||||
resp, err := resty.New().
|
||||
SetTimeout(time.Second * 3).R().
|
||||
SetHeaders(header).
|
||||
Get(url)
|
||||
return resp, err
|
||||
}
|
||||
func httpGetString(url string, header map[string]string) string {
|
||||
resp, err := httpGet(url, header)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return resp.String()
|
||||
}
|
||||
|
||||
func httpHead(url string, header map[string]string) (*resty.Response, error) {
|
||||
resp, err := resty.New().
|
||||
SetTimeout(time.Second * 3).R().
|
||||
SetHeaders(header).
|
||||
Head(url)
|
||||
return resp, err
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/adapter"
|
||||
"AynaLivePlayer/core/model"
|
||||
"fmt"
|
||||
"github.com/tidwall/gjson"
|
||||
"html"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Kuwo struct {
|
||||
InfoApi string
|
||||
FileApi string
|
||||
SearchCookie string
|
||||
SearchApi string
|
||||
LyricApi string
|
||||
PlaylistApi string
|
||||
PlaylistRegex0 *regexp.Regexp
|
||||
PlaylistRegex1 *regexp.Regexp
|
||||
IdRegex0 *regexp.Regexp
|
||||
IdRegex1 *regexp.Regexp
|
||||
}
|
||||
|
||||
func NewKuwo(config adapter.MediaProviderConfig) adapter.MediaProvider {
|
||||
return &Kuwo{
|
||||
InfoApi: "http://www.kuwo.cn/api/www/music/musicInfo?mid=%s&httpsStatus=1",
|
||||
//FileApi: "http://www.kuwo.cn/api/v1/www/music/playUrl?mid=%d&type=music&httpsStatus=1",
|
||||
FileApi: "http://antiserver.kuwo.cn/anti.s?type=convert_url&format=mp3&response=url&rid=MUSIC_%s",
|
||||
SearchCookie: "http://kuwo.cn/search/list?key=%s",
|
||||
SearchApi: "http://www.kuwo.cn/api/www/search/searchMusicBykeyWord?key=%s&pn=%d&rn=%d",
|
||||
LyricApi: "http://m.kuwo.cn/newh5/singles/songinfoandlrc?musicId=%s",
|
||||
PlaylistApi: "http://www.kuwo.cn/api/www/playlist/playListInfo?pid=%s&pn=%d&rn=%d&httpsStatus=1",
|
||||
PlaylistRegex0: regexp.MustCompile("[0-9]+"),
|
||||
PlaylistRegex1: regexp.MustCompile("playlist/[0-9]+"),
|
||||
IdRegex0: regexp.MustCompile("^[0-9]+"),
|
||||
IdRegex1: regexp.MustCompile("^kw[0-9]+"),
|
||||
}
|
||||
}
|
||||
|
||||
func _newKuwo() *Kuwo {
|
||||
return &Kuwo{
|
||||
InfoApi: "http://www.kuwo.cn/api/www/music/musicInfo?mid=%s&httpsStatus=1",
|
||||
//FileApi: "http://www.kuwo.cn/api/v1/www/music/playUrl?mid=%d&type=music&httpsStatus=1",
|
||||
FileApi: "http://antiserver.kuwo.cn/anti.s?type=convert_url&format=mp3&response=url&rid=MUSIC_%s",
|
||||
SearchCookie: "http://kuwo.cn/search/list?key=%s",
|
||||
SearchApi: "http://www.kuwo.cn/api/www/search/searchMusicBykeyWord?key=%s&pn=%d&rn=%d",
|
||||
LyricApi: "http://m.kuwo.cn/newh5/singles/songinfoandlrc?musicId=%s",
|
||||
PlaylistApi: "http://www.kuwo.cn/api/www/playlist/playListInfo?pid=%s&pn=%d&rn=%d&httpsStatus=1",
|
||||
PlaylistRegex0: regexp.MustCompile("[0-9]+"),
|
||||
PlaylistRegex1: regexp.MustCompile("playlist/[0-9]+"),
|
||||
IdRegex0: regexp.MustCompile("^[0-9]+"),
|
||||
IdRegex1: regexp.MustCompile("^kw[0-9]+"),
|
||||
}
|
||||
}
|
||||
|
||||
var KuwoAPI *Kuwo
|
||||
|
||||
func init() {
|
||||
KuwoAPI = _newKuwo()
|
||||
Providers[KuwoAPI.GetName()] = KuwoAPI
|
||||
}
|
||||
|
||||
func (k *Kuwo) GetName() string {
|
||||
return "kuwo"
|
||||
}
|
||||
|
||||
func (k *Kuwo) MatchMedia(keyword string) *model.Media {
|
||||
if id := k.IdRegex0.FindString(keyword); id != "" {
|
||||
return &model.Media{
|
||||
Meta: model.Meta{
|
||||
Name: k.GetName(),
|
||||
Id: id,
|
||||
},
|
||||
}
|
||||
}
|
||||
if id := k.IdRegex1.FindString(keyword); id != "" {
|
||||
return &model.Media{
|
||||
Meta: model.Meta{
|
||||
Name: k.GetName(),
|
||||
Id: id[2:],
|
||||
},
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *Kuwo) FormatPlaylistUrl(uri string) string {
|
||||
var id string
|
||||
id = k.PlaylistRegex0.FindString(uri)
|
||||
if id != "" {
|
||||
return id
|
||||
}
|
||||
id = k.PlaylistRegex1.FindString(uri)
|
||||
if id != "" {
|
||||
return id[9:]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
//func (k *Kuwo) _kuwoGet(url string) string {
|
||||
// searchCookie, err := httpHead(fmt.Sprintf(k.SearchCookie, "any"), nil)
|
||||
// if err != nil {
|
||||
// return ""
|
||||
// }
|
||||
// kwToken, ok := util.SliceString(regexp.MustCompile("kw_token=([^;])*;").FindString(searchCookie.Header().Get("set-cookie")), 9, -1)
|
||||
// if !ok {
|
||||
// return ""
|
||||
// }
|
||||
// return httpGetString(url, map[string]string{
|
||||
// "cookie": "kw_token=" + kwToken,
|
||||
// "csrf": kwToken,
|
||||
// "referer": "http://www.kuwo.cn/",
|
||||
// })
|
||||
//}
|
||||
|
||||
func (k *Kuwo) _kuwoGet(url string) string {
|
||||
return httpGetString(url, map[string]string{
|
||||
"cookie": "kw_token=" + "95MWTYC4FP",
|
||||
"csrf": "95MWTYC4FP",
|
||||
"referer": "http://www.kuwo.cn/",
|
||||
})
|
||||
}
|
||||
|
||||
func (k *Kuwo) Search(keyword string) ([]*model.Media, error) {
|
||||
resp := k._kuwoGet(fmt.Sprintf(k.SearchApi, url.QueryEscape(keyword), 1, 64))
|
||||
if resp == "" {
|
||||
return nil, ErrorExternalApi
|
||||
}
|
||||
result := make([]*model.Media, 0)
|
||||
gjson.Parse(resp).Get("data.list").ForEach(func(key, value gjson.Result) bool {
|
||||
result = append(result, &model.Media{
|
||||
Title: html.UnescapeString(value.Get("name").String()),
|
||||
Cover: model.Picture{Url: value.Get("pic").String()},
|
||||
Artist: value.Get("artist").String(),
|
||||
Album: value.Get("album").String(),
|
||||
Meta: model.Meta{
|
||||
Name: k.GetName(),
|
||||
Id: value.Get("rid").String(),
|
||||
},
|
||||
})
|
||||
return true
|
||||
})
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (k *Kuwo) UpdateMedia(media *model.Media) error {
|
||||
resp := k._kuwoGet(fmt.Sprintf(k.InfoApi, media.Meta.(model.Meta).Id))
|
||||
if resp == "" {
|
||||
return ErrorExternalApi
|
||||
}
|
||||
jresp := gjson.Parse(resp)
|
||||
if jresp.Get("data.musicrid").String() == "" {
|
||||
return ErrorExternalApi
|
||||
}
|
||||
media.Title = html.UnescapeString(jresp.Get("data.name").String())
|
||||
media.Cover.Url = jresp.Get("data.pic").String()
|
||||
media.Artist = jresp.Get("data.artist").String()
|
||||
media.Album = jresp.Get("data.album").String()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *Kuwo) UpdateMediaUrl(media *model.Media) error {
|
||||
result := httpGetString(fmt.Sprintf(k.FileApi, media.Meta.(model.Meta).Id), nil)
|
||||
if result == "" {
|
||||
return ErrorExternalApi
|
||||
}
|
||||
media.Url = result
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *Kuwo) UpdateMediaLyric(media *model.Media) error {
|
||||
result := httpGetString(fmt.Sprintf(k.LyricApi, media.Meta.(model.Meta).Id), nil)
|
||||
if result == "" {
|
||||
return ErrorExternalApi
|
||||
}
|
||||
lrcs := make([]string, 0)
|
||||
gjson.Parse(result).Get("data.lrclist").ForEach(func(key, value gjson.Result) bool {
|
||||
lrcs = append(lrcs, fmt.Sprintf("[00:%s]%s", value.Get("time").String(), value.Get("lineLyric").String()))
|
||||
|
||||
return true
|
||||
})
|
||||
media.Lyric = strings.Join(lrcs, "\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *Kuwo) GetPlaylist(playlist *model.Meta) ([]*model.Media, error) {
|
||||
medias := make([]*model.Media, 0)
|
||||
var resp string
|
||||
var jresp gjson.Result
|
||||
for i := 1; i <= 20; i++ {
|
||||
resp = k._kuwoGet(fmt.Sprintf(k.PlaylistApi, playlist.Id, i, 128))
|
||||
if resp == "" {
|
||||
break
|
||||
}
|
||||
//fmt.Println(resp[:100])
|
||||
jresp = gjson.Parse(resp)
|
||||
//fmt.Println(jresp.Get("code").String())
|
||||
if jresp.Get("code").String() != "200" {
|
||||
break
|
||||
}
|
||||
cnt := int(jresp.Get("data.total").Int())
|
||||
//fmt.Println(cnt)
|
||||
//fmt.Println(len(jresp.Get("data.musicList").Array()))
|
||||
jresp.Get("data.musicList").ForEach(func(key, value gjson.Result) bool {
|
||||
medias = append(
|
||||
medias,
|
||||
&model.Media{
|
||||
Title: html.UnescapeString(value.Get("name").String()),
|
||||
Artist: value.Get("artist").String(),
|
||||
Cover: model.Picture{Url: value.Get("pic").String()},
|
||||
Album: value.Get("album").String(),
|
||||
Meta: model.Meta{
|
||||
Name: k.GetName(),
|
||||
Id: value.Get("rid").String(),
|
||||
},
|
||||
})
|
||||
return true
|
||||
})
|
||||
if cnt <= i*100 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(medias) == 0 {
|
||||
return nil, ErrorExternalApi
|
||||
}
|
||||
return medias, nil
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/adapter"
|
||||
"AynaLivePlayer/core/model"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestKuwo_Search(t *testing.T) {
|
||||
var api adapter.MediaProvider = KuwoAPI
|
||||
result, err := api.Search("染 reol")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fmt.Println(result)
|
||||
media := result[0]
|
||||
err = api.UpdateMediaUrl(media)
|
||||
fmt.Println(err)
|
||||
fmt.Println(media.Url)
|
||||
}
|
||||
|
||||
func TestKuwo_GetMusicMeta(t *testing.T) {
|
||||
var api adapter.MediaProvider = KuwoAPI
|
||||
|
||||
media := model.Media{
|
||||
Meta: model.Meta{
|
||||
Name: api.GetName(),
|
||||
Id: "22804772",
|
||||
},
|
||||
}
|
||||
err := api.UpdateMedia(&media)
|
||||
fmt.Println(err)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fmt.Println(media)
|
||||
}
|
||||
|
||||
func TestKuwo_GetMusic(t *testing.T) {
|
||||
var api adapter.MediaProvider = KuwoAPI
|
||||
media := model.Media{
|
||||
Meta: model.Meta{
|
||||
Name: api.GetName(),
|
||||
Id: "22804772",
|
||||
},
|
||||
}
|
||||
err := api.UpdateMedia(&media)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = api.UpdateMediaUrl(&media)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fmt.Println(media)
|
||||
fmt.Println(media.Url)
|
||||
}
|
||||
|
||||
func TestKuwo_UpdateMediaLyric(t *testing.T) {
|
||||
var api adapter.MediaProvider = KuwoAPI
|
||||
media := model.Media{
|
||||
Meta: model.Meta{
|
||||
Name: api.GetName(),
|
||||
Id: "22804772",
|
||||
},
|
||||
}
|
||||
err := api.UpdateMediaLyric(&media)
|
||||
fmt.Println(err)
|
||||
fmt.Println(media.Lyric)
|
||||
}
|
||||
|
||||
func TestKuwo_GetPlaylist(t *testing.T) {
|
||||
var api adapter.MediaProvider = KuwoAPI
|
||||
playlist, err := api.GetPlaylist(&model.Meta{
|
||||
Name: api.GetName(),
|
||||
//Id: "1082685104",
|
||||
Id: "2959147566",
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println(len(playlist))
|
||||
for _, media := range playlist {
|
||||
fmt.Println(media.Title, media.Artist, media.Album)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/common/util"
|
||||
"AynaLivePlayer/core/adapter"
|
||||
"AynaLivePlayer/core/model"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type _LocalPlaylist struct {
|
||||
Name string
|
||||
Medias []*model.Media
|
||||
}
|
||||
|
||||
type Local struct {
|
||||
localDir string
|
||||
Playlists []*_LocalPlaylist
|
||||
}
|
||||
|
||||
func NewLocalCtor(config adapter.MediaProviderConfig) adapter.MediaProvider {
|
||||
localDir, ok := config["local_dir"]
|
||||
if !ok {
|
||||
localDir = "./local"
|
||||
}
|
||||
l := &Local{Playlists: make([]*_LocalPlaylist, 0), localDir: localDir}
|
||||
if err := os.MkdirAll(localDir, 0755); err != nil {
|
||||
return l
|
||||
}
|
||||
for _, n := range getPlaylistNames(localDir) {
|
||||
l.Playlists = append(l.Playlists, &_LocalPlaylist{Name: n})
|
||||
}
|
||||
for i, _ := range l.Playlists {
|
||||
_ = readLocalPlaylist(localDir, l.Playlists[i])
|
||||
}
|
||||
LocalAPI = l
|
||||
Providers[LocalAPI.GetName()] = LocalAPI
|
||||
return l
|
||||
}
|
||||
|
||||
var LocalAPI *Local
|
||||
|
||||
func NewLocal(localdir string) *Local {
|
||||
l := &Local{Playlists: make([]*_LocalPlaylist, 0), localDir: localdir}
|
||||
if err := os.MkdirAll(localdir, 0755); err != nil {
|
||||
return l
|
||||
}
|
||||
for _, n := range getPlaylistNames(localdir) {
|
||||
l.Playlists = append(l.Playlists, &_LocalPlaylist{Name: n})
|
||||
}
|
||||
for i, _ := range l.Playlists {
|
||||
_ = readLocalPlaylist(localdir, l.Playlists[i])
|
||||
}
|
||||
LocalAPI = l
|
||||
Providers[LocalAPI.GetName()] = LocalAPI
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *Local) GetName() string {
|
||||
return "local"
|
||||
}
|
||||
|
||||
func (l *Local) MatchMedia(keyword string) *model.Media {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Local) UpdateMediaLyric(media *model.Media) error {
|
||||
// already update in UpdateMedia, do nothing
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Local) FormatPlaylistUrl(uri string) string {
|
||||
return uri
|
||||
}
|
||||
|
||||
func (l *Local) GetPlaylist(playlist *model.Meta) ([]*model.Media, error) {
|
||||
var pl *_LocalPlaylist = nil
|
||||
for _, p := range l.Playlists {
|
||||
if p.Name == playlist.Id {
|
||||
pl = p
|
||||
}
|
||||
}
|
||||
if pl == nil {
|
||||
l.Playlists = append(l.Playlists, &_LocalPlaylist{Name: playlist.Id})
|
||||
pl = l.Playlists[len(l.Playlists)-1]
|
||||
}
|
||||
if readLocalPlaylist(l.localDir, pl) != nil {
|
||||
return nil, ErrorExternalApi
|
||||
}
|
||||
return pl.Medias, nil
|
||||
}
|
||||
|
||||
func (l *Local) Search(keyword string) ([]*model.Media, error) {
|
||||
allMedias := make([]*model.Media, 0)
|
||||
for _, p := range l.Playlists {
|
||||
for _, m := range p.Medias {
|
||||
allMedias = append(allMedias, m)
|
||||
}
|
||||
}
|
||||
MediaSort(keyword, allMedias)
|
||||
c := util.Min(len(allMedias), 32)
|
||||
medias := make([]*model.Media, c)
|
||||
for i := 0; i < c; i++ {
|
||||
medias[i] = allMedias[i].Copy()
|
||||
}
|
||||
return medias, nil
|
||||
}
|
||||
|
||||
func (l *Local) SearchV1(keyword string) ([]*model.Media, error) {
|
||||
result := make([]struct {
|
||||
M *model.Media
|
||||
N int
|
||||
}, 0)
|
||||
keywords := strings.Split(keyword, " ")
|
||||
for _, p := range l.Playlists {
|
||||
for _, m := range p.Medias {
|
||||
title := strings.ToLower(m.Title)
|
||||
artist := strings.ToLower(m.Artist)
|
||||
n := 0
|
||||
for _, k := range keywords {
|
||||
kw := strings.ToLower(k)
|
||||
if strings.Contains(title, kw) || strings.Contains(artist, kw) {
|
||||
n++
|
||||
}
|
||||
if kw == title {
|
||||
n += 3
|
||||
}
|
||||
}
|
||||
if n > 0 {
|
||||
result = append(result, struct {
|
||||
M *model.Media
|
||||
N int
|
||||
}{M: m, N: n})
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return result[i].N > result[j].N
|
||||
})
|
||||
medias := make([]*model.Media, len(result))
|
||||
for i, r := range result {
|
||||
medias[i] = r.M.Copy()
|
||||
}
|
||||
return medias, nil
|
||||
}
|
||||
|
||||
func (l *Local) UpdateMedia(media *model.Media) error {
|
||||
mediaPath := media.Meta.(model.Meta).Id
|
||||
_, err := os.Stat(mediaPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return readMediaFile(media)
|
||||
}
|
||||
|
||||
func (l *Local) UpdateMediaUrl(media *model.Media) error {
|
||||
mediaPath := media.Meta.(model.Meta).Id
|
||||
_, err := os.Stat(mediaPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
media.Url = mediaPath
|
||||
return nil
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/common/util"
|
||||
"AynaLivePlayer/core/model"
|
||||
"github.com/dhowden/tag"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func getPlaylistNames(localdir string) []string {
|
||||
names := make([]string, 0)
|
||||
items, _ := ioutil.ReadDir(localdir)
|
||||
for _, item := range items {
|
||||
if item.IsDir() {
|
||||
names = append(names, item.Name())
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// readLocalPlaylist read files under a directory
|
||||
// and return a _LocalPlaylist object.
|
||||
// This function assume this directory exists
|
||||
func readLocalPlaylist(localdir string, playlist *_LocalPlaylist) error {
|
||||
p1th := playlist.Name
|
||||
playlist.Medias = make([]*model.Media, 0)
|
||||
fullPath := filepath.Join(localdir, p1th)
|
||||
if _, err := os.Stat(fullPath); os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
items, _ := ioutil.ReadDir(fullPath)
|
||||
for _, item := range items {
|
||||
// if item is a file, read file
|
||||
if !item.IsDir() {
|
||||
fn := item.Name()
|
||||
media := model.Media{
|
||||
Meta: model.Meta{
|
||||
Name: LocalAPI.GetName(),
|
||||
Id: filepath.Join(fullPath, fn),
|
||||
},
|
||||
}
|
||||
if readMediaFile(&media) != nil {
|
||||
continue
|
||||
}
|
||||
playlist.Medias = append(playlist.Medias, &media)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readMediaFile(media *model.Media) error {
|
||||
p := media.Meta.(model.Meta).Id
|
||||
f, err := os.Open(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
meta, err := tag.ReadFrom(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
media.Title = util.GetOrDefault(meta.Title(), filepath.Base(p))
|
||||
media.Artist = util.GetOrDefault(meta.Artist(), "Unknown")
|
||||
media.Album = util.GetOrDefault(meta.Album(), "Unknown")
|
||||
media.Lyric = meta.Lyrics()
|
||||
if meta.Picture() != nil {
|
||||
media.Cover.Data = meta.Picture().Data
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLocal_Read(t *testing.T) {
|
||||
items, _ := ioutil.ReadDir(".")
|
||||
for _, item := range items {
|
||||
if item.IsDir() {
|
||||
subitems, _ := ioutil.ReadDir(item.Name())
|
||||
for _, subitem := range subitems {
|
||||
if !subitem.IsDir() {
|
||||
// handle file there
|
||||
fmt.Println(item.Name() + "/" + subitem.Name())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// handle file there
|
||||
fmt.Println(item.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/common/util"
|
||||
"AynaLivePlayer/core/adapter"
|
||||
"AynaLivePlayer/core/model"
|
||||
neteaseApi "github.com/XiaoMengXinX/Music163Api-Go/api"
|
||||
neteaseTypes "github.com/XiaoMengXinX/Music163Api-Go/types"
|
||||
neteaseUtil "github.com/XiaoMengXinX/Music163Api-Go/utils"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Netease struct {
|
||||
PlaylistRegex0 *regexp.Regexp
|
||||
PlaylistRegex1 *regexp.Regexp
|
||||
ReqData neteaseUtil.RequestData
|
||||
IdRegex0 *regexp.Regexp
|
||||
IdRegex1 *regexp.Regexp
|
||||
loginStatus neteaseTypes.LoginStatusData
|
||||
}
|
||||
|
||||
func NewNetease(config adapter.MediaProviderConfig) adapter.MediaProvider {
|
||||
return &Netease{
|
||||
PlaylistRegex0: regexp.MustCompile("^[0-9]+$"),
|
||||
// https://music.163.com/playlist?id=2382819181&userid=95906480
|
||||
PlaylistRegex1: regexp.MustCompile("playlist\\?id=[0-9]+"),
|
||||
ReqData: neteaseUtil.RequestData{
|
||||
Headers: neteaseUtil.Headers{
|
||||
{
|
||||
"X-Real-IP",
|
||||
"118.88.88.88",
|
||||
},
|
||||
},
|
||||
},
|
||||
IdRegex0: regexp.MustCompile("^[0-9]+"),
|
||||
IdRegex1: regexp.MustCompile("^wy[0-9]+"),
|
||||
}
|
||||
}
|
||||
|
||||
func _newNetease() *Netease {
|
||||
return &Netease{
|
||||
PlaylistRegex0: regexp.MustCompile("^[0-9]+$"),
|
||||
// https://music.163.com/playlist?id=2382819181&userid=95906480
|
||||
PlaylistRegex1: regexp.MustCompile("playlist\\?id=[0-9]+"),
|
||||
ReqData: neteaseUtil.RequestData{
|
||||
Headers: neteaseUtil.Headers{
|
||||
{
|
||||
"X-Real-IP",
|
||||
"118.88.88.88",
|
||||
},
|
||||
},
|
||||
},
|
||||
IdRegex0: regexp.MustCompile("^[0-9]+"),
|
||||
IdRegex1: regexp.MustCompile("^wy[0-9]+"),
|
||||
}
|
||||
}
|
||||
|
||||
var NeteaseAPI *Netease
|
||||
|
||||
func init() {
|
||||
NeteaseAPI = _newNetease()
|
||||
Providers[NeteaseAPI.GetName()] = NeteaseAPI
|
||||
}
|
||||
|
||||
// Netease private helper method
|
||||
|
||||
func _neteaseGetArtistNames(data neteaseTypes.SongDetailData) string {
|
||||
artists := make([]string, 0)
|
||||
for _, a := range data.Ar {
|
||||
artists = append(artists, a.Name)
|
||||
}
|
||||
return strings.Join(artists, ",")
|
||||
}
|
||||
|
||||
// MediaProvider implementation
|
||||
|
||||
func (n *Netease) GetName() string {
|
||||
return "netease"
|
||||
}
|
||||
|
||||
func (n *Netease) MatchMedia(keyword string) *model.Media {
|
||||
if id := n.IdRegex0.FindString(keyword); id != "" {
|
||||
return &model.Media{
|
||||
Meta: model.Meta{
|
||||
Name: n.GetName(),
|
||||
Id: id,
|
||||
},
|
||||
}
|
||||
}
|
||||
if id := n.IdRegex1.FindString(keyword); id != "" {
|
||||
return &model.Media{
|
||||
Meta: model.Meta{
|
||||
Name: n.GetName(),
|
||||
Id: id[2:],
|
||||
},
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Netease) FormatPlaylistUrl(uri string) string {
|
||||
var id string
|
||||
id = n.PlaylistRegex0.FindString(uri)
|
||||
if id != "" {
|
||||
return id
|
||||
}
|
||||
id = n.PlaylistRegex1.FindString(uri)
|
||||
if id != "" {
|
||||
return id[12:]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (n *Netease) GetPlaylist(playlist *model.Meta) ([]*model.Media, error) {
|
||||
result, err := neteaseApi.GetPlaylistDetail(
|
||||
n.ReqData, util.Atoi(playlist.Id))
|
||||
if err != nil || result.Code != 200 {
|
||||
return nil, ErrorExternalApi
|
||||
}
|
||||
cnt := len(result.Playlist.TrackIds)
|
||||
if cnt == 0 {
|
||||
return nil, ErrorExternalApi
|
||||
}
|
||||
|
||||
ids := make([]int, len(result.Playlist.TrackIds))
|
||||
for i := 0; i < cnt; i++ {
|
||||
ids[i] = result.Playlist.TrackIds[i].Id
|
||||
}
|
||||
medias := make([]*model.Media, 0, cnt)
|
||||
for index := 0; index < len(ids); index += 1000 {
|
||||
result2, err := neteaseApi.GetSongDetail(
|
||||
n.ReqData,
|
||||
ids[index:util.IntMin(index+1000, len(ids))])
|
||||
if err != nil || result2.Code != 200 {
|
||||
break
|
||||
}
|
||||
cnt = len(result2.Songs)
|
||||
if cnt == 0 {
|
||||
break
|
||||
}
|
||||
for i := 0; i < cnt; i++ {
|
||||
medias = append(medias, &model.Media{
|
||||
Title: result2.Songs[i].Name,
|
||||
Artist: _neteaseGetArtistNames(result2.Songs[i]),
|
||||
Cover: model.Picture{Url: result2.Songs[i].Al.PicUrl},
|
||||
Album: result2.Songs[i].Al.Name,
|
||||
Url: "",
|
||||
Header: nil,
|
||||
User: nil,
|
||||
Meta: model.Meta{
|
||||
Name: n.GetName(),
|
||||
Id: strconv.Itoa(result2.Songs[i].Id),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(medias) == 0 {
|
||||
return nil, ErrorExternalApi
|
||||
}
|
||||
return medias, nil
|
||||
}
|
||||
|
||||
func (n *Netease) Search(keyword string) ([]*model.Media, error) {
|
||||
rawResult, err := neteaseApi.SearchSong(
|
||||
n.ReqData,
|
||||
neteaseApi.SearchSongConfig{
|
||||
Keyword: keyword,
|
||||
Limit: 30,
|
||||
Offset: 0,
|
||||
})
|
||||
if err != nil || rawResult.Code != 200 {
|
||||
return nil, ErrorExternalApi
|
||||
}
|
||||
medias := make([]*model.Media, 0)
|
||||
for _, song := range rawResult.Result.Songs {
|
||||
artists := make([]string, 0)
|
||||
for _, a := range song.Artists {
|
||||
artists = append(artists, a.Name)
|
||||
}
|
||||
medias = append(medias, &model.Media{
|
||||
Title: song.Name,
|
||||
Artist: strings.Join(artists, ","),
|
||||
Cover: model.Picture{},
|
||||
Album: song.Album.Name,
|
||||
Url: "",
|
||||
Header: nil,
|
||||
Meta: model.Meta{
|
||||
Name: n.GetName(),
|
||||
Id: strconv.Itoa(song.Id),
|
||||
},
|
||||
})
|
||||
}
|
||||
return medias, nil
|
||||
}
|
||||
|
||||
func (n *Netease) UpdateMedia(media *model.Media) error {
|
||||
result, err := neteaseApi.GetSongDetail(
|
||||
n.ReqData,
|
||||
[]int{util.Atoi(media.Meta.(model.Meta).Id)})
|
||||
if err != nil || result.Code != 200 {
|
||||
return ErrorExternalApi
|
||||
}
|
||||
if len(result.Songs) == 0 {
|
||||
return ErrorExternalApi
|
||||
}
|
||||
media.Title = result.Songs[0].Name
|
||||
media.Cover.Url = result.Songs[0].Al.PicUrl
|
||||
media.Album = result.Songs[0].Al.Name
|
||||
media.Artist = _neteaseGetArtistNames(result.Songs[0])
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Netease) UpdateMediaUrl(media *model.Media) error {
|
||||
result, err := neteaseApi.GetSongURL(
|
||||
n.ReqData,
|
||||
neteaseApi.SongURLConfig{Ids: []int{util.Atoi(media.Meta.(model.Meta).Id)}})
|
||||
if err != nil || result.Code != 200 {
|
||||
return ErrorExternalApi
|
||||
}
|
||||
if len(result.Data) == 0 {
|
||||
return ErrorExternalApi
|
||||
}
|
||||
if result.Data[0].Code != 200 {
|
||||
return ErrorExternalApi
|
||||
}
|
||||
media.Url = result.Data[0].Url
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Netease) UpdateMediaLyric(media *model.Media) error {
|
||||
result, err := neteaseApi.GetSongLyric(n.ReqData, util.Atoi(media.Meta.(model.Meta).Id))
|
||||
if err != nil || result.Code != 200 {
|
||||
return ErrorExternalApi
|
||||
}
|
||||
media.Lyric = result.Lrc.Lyric
|
||||
return nil
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
neteaseApi "github.com/XiaoMengXinX/Music163Api-Go/api"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Netease other method
|
||||
|
||||
func (n *Netease) UpdateStatus() {
|
||||
status, _ := neteaseApi.GetLoginStatus(n.ReqData)
|
||||
n.loginStatus = status
|
||||
}
|
||||
|
||||
// IsLogin check if current cookie is a login user
|
||||
func (n *Netease) IsLogin() bool {
|
||||
return n.loginStatus.Profile.UserId != 0
|
||||
}
|
||||
|
||||
func (n *Netease) Nickname() string {
|
||||
return n.loginStatus.Profile.Nickname
|
||||
}
|
||||
|
||||
func (n *Netease) GetQrLoginKey() string {
|
||||
unikey, err := neteaseApi.GetQrUnikey(n.ReqData)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return unikey.Unikey
|
||||
}
|
||||
|
||||
func (n *Netease) GetQrLoginUrl(key string) string {
|
||||
return fmt.Sprintf("https://music.163.com/login?codekey=%s", key)
|
||||
}
|
||||
|
||||
func (n *Netease) CheckQrLogin(key string) (bool, string) {
|
||||
login, h, err := neteaseApi.CheckQrLogin(n.ReqData, key)
|
||||
if err != nil {
|
||||
return false, ""
|
||||
}
|
||||
// if login.Code == 800 || login.Code == 803. login success
|
||||
if login.Code != 800 && login.Code != 803 {
|
||||
return false, login.Message
|
||||
}
|
||||
cookies := make([]*http.Cookie, 0)
|
||||
for _, c := range (&http.Response{Header: h}).Cookies() {
|
||||
if c.Name == "MUSIC_U" || c.Name == "__csrf" {
|
||||
cookies = append(cookies, c)
|
||||
}
|
||||
}
|
||||
n.ReqData.Cookies = cookies
|
||||
return true, login.Message
|
||||
}
|
||||
|
||||
func (n *Netease) Logout() {
|
||||
n.ReqData.Cookies = []*http.Cookie{
|
||||
{Name: "MUSIC_U", Value: ""},
|
||||
{Name: "__csrf", Value: ""},
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/adapter"
|
||||
"AynaLivePlayer/core/model"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNetease_Search(t *testing.T) {
|
||||
var api adapter.MediaProvider = NeteaseAPI
|
||||
result, err := api.Search("染 reol")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fmt.Println(result)
|
||||
media := result[0]
|
||||
fmt.Println(media)
|
||||
err = api.UpdateMediaUrl(media)
|
||||
fmt.Println(err)
|
||||
fmt.Println(media.Url)
|
||||
}
|
||||
|
||||
func TestNetease_Search2(t *testing.T) {
|
||||
var api adapter.MediaProvider = NeteaseAPI
|
||||
result, err := api.Search("出山")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.Log(result)
|
||||
media := result[0]
|
||||
t.Log(media)
|
||||
err = api.UpdateMediaUrl(media)
|
||||
t.Log(err)
|
||||
t.Log(media.Url)
|
||||
}
|
||||
|
||||
func TestNetease_GetMusicMeta(t *testing.T) {
|
||||
var api adapter.MediaProvider = NeteaseAPI
|
||||
|
||||
media := model.Media{
|
||||
Meta: model.Meta{
|
||||
Name: api.GetName(),
|
||||
Id: "33516503",
|
||||
},
|
||||
}
|
||||
err := api.UpdateMedia(&media)
|
||||
fmt.Println(err)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fmt.Println(media)
|
||||
}
|
||||
|
||||
func TestNetease_GetMusic(t *testing.T) {
|
||||
var api adapter.MediaProvider = NeteaseAPI
|
||||
media := model.Media{
|
||||
Meta: model.Meta{
|
||||
Name: api.GetName(),
|
||||
Id: "33516503",
|
||||
},
|
||||
}
|
||||
err := api.UpdateMedia(&media)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = api.UpdateMediaUrl(&media)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fmt.Println(media)
|
||||
fmt.Println(media.Url)
|
||||
}
|
||||
|
||||
func TestNetease_GetPlaylist(t *testing.T) {
|
||||
var api adapter.MediaProvider = NeteaseAPI
|
||||
playlist, err := api.GetPlaylist(&model.Meta{
|
||||
Name: api.GetName(),
|
||||
//Id: "2520739691",
|
||||
Id: "2382819181",
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println(len(playlist))
|
||||
for _, media := range playlist {
|
||||
fmt.Println(media.Title, media.Artist, media.Album)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestNetease_UpdateMediaLyric(t *testing.T) {
|
||||
var api adapter.MediaProvider = NeteaseAPI
|
||||
media := model.Media{
|
||||
Meta: model.Meta{
|
||||
Name: api.GetName(),
|
||||
Id: "33516503",
|
||||
},
|
||||
}
|
||||
err := api.UpdateMediaLyric(&media)
|
||||
fmt.Println(err)
|
||||
fmt.Println(media.Lyric)
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/adapter"
|
||||
model "AynaLivePlayer/core/model"
|
||||
)
|
||||
|
||||
var RegisteredProviders = map[string]adapter.MediaProviderCtor{
|
||||
"netease": NewNetease,
|
||||
"local": NewLocalCtor,
|
||||
"kuwo": NewKuwo,
|
||||
"bilibili": NewBilibili,
|
||||
"bilibili-video": NewBilibiliVideo,
|
||||
}
|
||||
|
||||
var Providers map[string]adapter.MediaProvider = make(map[string]adapter.MediaProvider)
|
||||
|
||||
func GetPlaylist(meta *model.Meta) ([]*model.Media, error) {
|
||||
if v, ok := Providers[meta.Name]; ok {
|
||||
return v.GetPlaylist(meta)
|
||||
}
|
||||
return nil, ErrorNoSuchProvider
|
||||
}
|
||||
|
||||
func FormatPlaylistUrl(pname, uri string) (string, error) {
|
||||
if v, ok := Providers[pname]; ok {
|
||||
return v.FormatPlaylistUrl(uri), nil
|
||||
}
|
||||
return "", ErrorNoSuchProvider
|
||||
}
|
||||
|
||||
func MatchMedia(provider string, keyword string) *model.Media {
|
||||
if v, ok := Providers[provider]; ok {
|
||||
return v.MatchMedia(keyword)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Search(provider string, keyword string) ([]*model.Media, error) {
|
||||
if v, ok := Providers[provider]; ok {
|
||||
return v.Search(keyword)
|
||||
}
|
||||
return nil, ErrorNoSuchProvider
|
||||
}
|
||||
|
||||
func UpdateMedia(media *model.Media) error {
|
||||
if v, ok := Providers[media.Meta.(model.Meta).Name]; ok {
|
||||
return v.UpdateMedia(media)
|
||||
}
|
||||
return ErrorNoSuchProvider
|
||||
}
|
||||
|
||||
func UpdateMediaUrl(media *model.Media) error {
|
||||
if v, ok := Providers[media.Meta.(model.Meta).Name]; ok {
|
||||
return v.UpdateMediaUrl(media)
|
||||
}
|
||||
return ErrorNoSuchProvider
|
||||
}
|
||||
|
||||
func UpdateMediaLyric(media *model.Media) error {
|
||||
if v, ok := Providers[media.Meta.(model.Meta).Name]; ok {
|
||||
return v.UpdateMediaLyric(media)
|
||||
}
|
||||
return ErrorNoSuchProvider
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/common/util"
|
||||
"AynaLivePlayer/core/model"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func MediaSort(keyword string, medias []*model.Media) {
|
||||
mediaDist := make([]struct {
|
||||
media *model.Media
|
||||
dist int
|
||||
}, len(medias))
|
||||
for i, media := range medias {
|
||||
mediaDist[i].media = media
|
||||
mediaDist[i].dist = util.StrLen(util.LongestCommonString(keyword, media.Title)) +
|
||||
util.StrLen(util.LongestCommonString(keyword, media.Artist))
|
||||
}
|
||||
sort.Slice(mediaDist, func(i, j int) bool {
|
||||
return mediaDist[i].dist > mediaDist[j].dist
|
||||
})
|
||||
for i, media := range mediaDist {
|
||||
medias[i] = media.media
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/app"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/data/binding"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var w fyne.Window
|
||||
|
||||
func main() {
|
||||
a := app.NewWithID("io.fyne.demo")
|
||||
a.SetIcon(theme.FyneLogo())
|
||||
w = a.NewWindow("Fyne Demo")
|
||||
Regen(w)
|
||||
w.Resize(fyne.NewSize(1080, 720))
|
||||
w.ShowAndRun()
|
||||
}
|
||||
|
||||
func Regen(w fyne.Window) {
|
||||
tabs := container.NewDocTabs()
|
||||
for _, datum := range generateData(100) {
|
||||
tabs.Append(newItemTab(&datum))
|
||||
}
|
||||
w.SetContent(tabs)
|
||||
}
|
||||
|
||||
func generateData(n int) (result []int) {
|
||||
for i := 0; i < n; i++ {
|
||||
result = append(result, i)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func newItemTab(i *int) *container.TabItem {
|
||||
c := container.NewVBox(
|
||||
BindIntWithEntry(i),
|
||||
widget.NewButton("Regen", func() {
|
||||
Regen(w)
|
||||
}),
|
||||
)
|
||||
return container.NewTabItemWithIcon(strconv.Itoa(*i), theme.MenuIcon(), c)
|
||||
}
|
||||
|
||||
func BindIntWithLabel(k *int) *widget.Label {
|
||||
b := binding.BindInt(k)
|
||||
return widget.NewLabelWithData(binding.IntToString(b))
|
||||
}
|
||||
|
||||
func BindIntWithEntry(k *int) *widget.Entry {
|
||||
b := binding.BindInt(k)
|
||||
return widget.NewEntryWithData(binding.IntToString(b))
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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("")
|
||||
}
|
||||
}
|
||||
@@ -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}),
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"},
|
||||
}
|
||||
)
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()},
|
||||
}
|
||||
}
|
||||
@@ -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{}
|
||||
}
|
||||
@@ -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/")),
|
||||
),
|
||||
))
|
||||
}
|
||||
@@ -1,421 +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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
96
app/main.go
Normal file
96
app/main.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/core/model"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui"
|
||||
"AynaLivePlayer/gui/gctx"
|
||||
"AynaLivePlayer/internal"
|
||||
"AynaLivePlayer/pkg/config"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
"AynaLivePlayer/pkg/logger"
|
||||
|
||||
loggerRepo "AynaLivePlayer/pkg/logger/repository"
|
||||
"flag"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
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(eventbus.WithMaxWorkerSize(len(events.EventsMapping)))
|
||||
global.Logger = loggerRepo.NewZapColoredLogger(Log.Path, !*dev)
|
||||
global.Logger.SetLogLevel(Log.Level)
|
||||
}
|
||||
|
||||
func init() {
|
||||
flag.Parse()
|
||||
// if not dev, set working directory to executable directory
|
||||
if !*dev {
|
||||
exePath, _ := os.Executable()
|
||||
exePath, _ = filepath.EvalSymlinks(exePath)
|
||||
exeDir := filepath.Dir(exePath)
|
||||
_ = os.Chdir(exeDir)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
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()
|
||||
if *headless || config.Experimental.Headless {
|
||||
quit := make(chan os.Signal)
|
||||
signal.Notify(quit, os.Interrupt)
|
||||
_ = global.EventBus.Start()
|
||||
<-quit
|
||||
} else {
|
||||
gui.Initialize()
|
||||
_ = global.EventBus.Start()
|
||||
gctx.Context.Window.ShowAndRun()
|
||||
}
|
||||
global.Logger.Info("closing internal server")
|
||||
internal.Stop()
|
||||
global.Logger.Infof("closing event manager")
|
||||
_ = global.EventBus.Stop()
|
||||
_ = global.EventBus.Wait()
|
||||
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
32
assets/config/diange.json
Normal 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
|
||||
}
|
||||
}
|
||||
12
assets/config/liverooms.json
Normal file
12
assets/config/liverooms.json
Normal 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
2230
assets/config/playlists.json
Normal file
File diff suppressed because one or more lines are too long
0
assets/deps/linux/.gitkeep
Normal file
0
assets/deps/linux/.gitkeep
Normal file
0
assets/deps/windows/.gitkeep
Normal file
0
assets/deps/windows/.gitkeep
Normal file
BIN
assets/empty.png
BIN
assets/empty.png
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 KiB |
BIN
assets/icon.jpg
BIN
assets/icon.jpg
Binary file not shown.
|
Before Width: | Height: | Size: 95 KiB |
BIN
assets/icon.png
Normal file
BIN
assets/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 113 KiB |
BIN
assets/icon2.png
Normal file
BIN
assets/icon2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 350 KiB |
BIN
assets/msyh.ttc
BIN
assets/msyh.ttc
Binary file not shown.
Binary file not shown.
0
assets/scripts/linux/.gitkeep
Normal file
0
assets/scripts/linux/.gitkeep
Normal file
68
assets/scripts/utils/ttc2ttf.py
Normal file
68
assets/scripts/utils/ttc2ttf.py
Normal 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 don’t 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)
|
||||
1
assets/scripts/windows/start-headless.bat
Normal file
1
assets/scripts/windows/start-headless.bat
Normal file
@@ -0,0 +1 @@
|
||||
start AynaLivePlayer.exe --headless
|
||||
1
assets/scripts/windows/stop-headless.bat
Normal file
1
assets/scripts/windows/stop-headless.bat
Normal file
@@ -0,0 +1 @@
|
||||
taskkill /F /IM AynaLivePlayer.exe
|
||||
@@ -4,6 +4,10 @@
|
||||
"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": "音频输出设备"
|
||||
@@ -36,14 +40,6 @@
|
||||
"en": "User Playlist",
|
||||
"zh-CN": "用户歌单"
|
||||
},
|
||||
"gui.config.basic.skip_playlist": {
|
||||
"en": "Skip Media From System Playlist",
|
||||
"zh-CN": "跳过闲置歌单"
|
||||
},
|
||||
"gui.config.basic.skip_playlist.prompt": {
|
||||
"en": "Skip",
|
||||
"zh-CN": "跳过"
|
||||
},
|
||||
"gui.config.basic.skip_when_error": {
|
||||
"en": "Skip this Media On Error",
|
||||
"zh-CN": "跳过获取失败的歌曲"
|
||||
@@ -56,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": "歌手"
|
||||
@@ -72,6 +76,10 @@
|
||||
"en": "User",
|
||||
"zh-CN": "用户"
|
||||
},
|
||||
"gui.lyric.title": {
|
||||
"en": "Lyric",
|
||||
"zh-CN": "歌词"
|
||||
},
|
||||
"gui.player.button.lrc": {
|
||||
"en": "lrc",
|
||||
"zh-CN": "歌词"
|
||||
@@ -168,6 +176,10 @@
|
||||
"en": "Room ID",
|
||||
"zh-CN": "房间号"
|
||||
},
|
||||
"gui.room.add.name": {
|
||||
"en": "Display Name",
|
||||
"zh-CN": "显示名"
|
||||
},
|
||||
"gui.room.add.prompt": {
|
||||
"en": "enter room id",
|
||||
"zh-CN": "填入房间号"
|
||||
@@ -268,21 +280,57 @@
|
||||
"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",
|
||||
@@ -312,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": "来源点歌命令"
|
||||
@@ -324,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",
|
||||
@@ -384,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": "开启"
|
||||
@@ -400,53 +500,93 @@
|
||||
"en": "Text Output",
|
||||
"zh-CN": "文本输出"
|
||||
},
|
||||
"plugin.webinfo.autostart": {
|
||||
"plugin.wshub.autostart": {
|
||||
"en": "Auto start",
|
||||
"zh-CN": "自动启用"
|
||||
},
|
||||
"plugin.webinfo.description": {
|
||||
"en": "Web output configuration",
|
||||
"zh-CN": "web输出设置"
|
||||
"plugin.wshub.description": {
|
||||
"en": "Websocket Hub Configuration",
|
||||
"zh-CN": "Websocket服务器设置"
|
||||
},
|
||||
"plugin.webinfo.port": {
|
||||
"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_preview": {
|
||||
"en": "Server Preview",
|
||||
"zh-CN": "效果预览"
|
||||
"plugin.wshub.server_link": {
|
||||
"en": "Websocket Server Link",
|
||||
"zh-CN": "Websocket服务器链接"
|
||||
},
|
||||
"plugin.webinfo.server_status": {
|
||||
"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.wshub.webinfo_text": {
|
||||
"en": "Obs browser output",
|
||||
"zh-CN": "OBS网页输出: "
|
||||
},
|
||||
"plugin.yinliang.admin_permission": {
|
||||
"en": "Admin only",
|
||||
"zh-CN": "仅房管可操作"
|
||||
},
|
||||
"plugin.yinliang.description": {
|
||||
"en": "Control volume via danmaku",
|
||||
"zh-CN": "通过弹幕控制音量"
|
||||
},
|
||||
"plugin.yinliang.enabled": {
|
||||
"en": "Enabled volume control",
|
||||
"zh-CN": "启用弹幕音量控制"
|
||||
},
|
||||
"plugin.yinliang.max_volume": {
|
||||
"en": "Maximum volume (%)",
|
||||
"zh-CN": "最大音量限制 (%)"
|
||||
},
|
||||
"plugin.yinliang.title": {
|
||||
"en": "Volume Control",
|
||||
"zh-CN": "音量控制"
|
||||
},
|
||||
"plugin.yinliang.volume_down_cmd": {
|
||||
"en": "Volume decrease command",
|
||||
"zh-CN": "音量减少命令"
|
||||
},
|
||||
"plugin.yinliang.volume_step": {
|
||||
"en": "Adjustment step (%)",
|
||||
"zh-CN": "每次音量调整 (%)"
|
||||
},
|
||||
"plugin.yinliang.volume_up_cmd": {
|
||||
"en": "Volume increase command",
|
||||
"zh-CN": "音量增加命令"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package config
|
||||
|
||||
type _GeneralConfig struct {
|
||||
BaseConfig
|
||||
Language string
|
||||
AutoCheckUpdate bool
|
||||
}
|
||||
|
||||
func (c *_GeneralConfig) Name() string {
|
||||
return "General"
|
||||
}
|
||||
|
||||
var General = &_GeneralConfig{
|
||||
Language: "en",
|
||||
AutoCheckUpdate: true,
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type EventId string
|
||||
|
||||
type Event struct {
|
||||
Id EventId
|
||||
Cancelled bool
|
||||
Data interface{}
|
||||
Outdated bool
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
type HandlerFunc func(event *Event)
|
||||
|
||||
type Handler struct {
|
||||
EventId EventId
|
||||
Name string
|
||||
Handler HandlerFunc
|
||||
SkipOutdated bool
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
handlers map[EventId]map[string]*Handler
|
||||
prevEvent map[EventId]*Event
|
||||
queue chan func()
|
||||
stopSig chan int
|
||||
queueSize int
|
||||
workerSize int
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func NewManger(queueSize int, workerSize int) *Manager {
|
||||
manager := &Manager{
|
||||
handlers: make(map[EventId]map[string]*Handler),
|
||||
prevEvent: make(map[EventId]*Event),
|
||||
queue: make(chan func(), queueSize),
|
||||
stopSig: make(chan int, workerSize),
|
||||
queueSize: queueSize,
|
||||
workerSize: workerSize,
|
||||
}
|
||||
for i := 0; i < workerSize; i++ {
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-manager.stopSig:
|
||||
return
|
||||
case f := <-manager.queue:
|
||||
f()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
return manager
|
||||
}
|
||||
|
||||
func (h *Manager) NewChildManager() *Manager {
|
||||
return &Manager{
|
||||
handlers: make(map[EventId]map[string]*Handler),
|
||||
prevEvent: make(map[EventId]*Event),
|
||||
queue: h.queue,
|
||||
stopSig: h.stopSig,
|
||||
queueSize: h.queueSize,
|
||||
workerSize: h.workerSize,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Manager) Stop() {
|
||||
for i := 0; i < h.workerSize; i++ {
|
||||
h.stopSig <- 0
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Manager) Register(handler *Handler) {
|
||||
h.lock.Lock()
|
||||
defer h.lock.Unlock()
|
||||
m, ok := h.handlers[handler.EventId]
|
||||
if !ok {
|
||||
m = make(map[string]*Handler)
|
||||
h.handlers[handler.EventId] = m
|
||||
}
|
||||
m[handler.Name] = handler
|
||||
}
|
||||
|
||||
func (h *Manager) RegisterA(id EventId, name string, handler HandlerFunc) {
|
||||
h.Register(&Handler{
|
||||
EventId: id,
|
||||
Name: name,
|
||||
Handler: handler,
|
||||
SkipOutdated: true,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Manager) UnregisterAll() {
|
||||
h.lock.Lock()
|
||||
defer h.lock.Unlock()
|
||||
h.handlers = make(map[EventId]map[string]*Handler)
|
||||
}
|
||||
|
||||
func (h *Manager) Unregister(name string) {
|
||||
h.lock.Lock()
|
||||
defer h.lock.Unlock()
|
||||
for _, m := range h.handlers {
|
||||
if _, ok := m[name]; ok {
|
||||
delete(m, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Manager) Call(event *Event) {
|
||||
h.lock.Lock()
|
||||
|
||||
handlers, ok := h.handlers[event.Id]
|
||||
if e := h.prevEvent[event.Id]; e != nil {
|
||||
e.lock.Lock()
|
||||
e.Outdated = true
|
||||
e.lock.Unlock()
|
||||
}
|
||||
h.prevEvent[event.Id] = event
|
||||
h.lock.Unlock()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
for _, eh := range handlers {
|
||||
eventHandler := eh
|
||||
h.queue <- func() {
|
||||
event.lock.Lock()
|
||||
if eventHandler.SkipOutdated && event.Outdated {
|
||||
event.lock.Unlock()
|
||||
return
|
||||
}
|
||||
eventHandler.Handler(event)
|
||||
event.lock.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Manager) CallA(id EventId, data interface{}) {
|
||||
h.Call(&Event{
|
||||
Id: id,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestEventSeq(t *testing.T) {
|
||||
m := NewManger(128, 16)
|
||||
m.RegisterA("ceshi", "asdf1", func(event *Event) {
|
||||
fmt.Println("Num:", event.Data)
|
||||
})
|
||||
go func() {
|
||||
for i := 0; i < 1000; i++ {
|
||||
m.CallA("ceshi", fmt.Sprintf("a%d", i))
|
||||
}
|
||||
}()
|
||||
for i := 0; i < 1000; i++ {
|
||||
m.CallA("ceshi", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventWeired(t *testing.T) {
|
||||
m := NewManger(128, 2)
|
||||
m.RegisterA("playlist.update", "asdf1", func(event *Event) {
|
||||
fmt.Printf("%d %p, outdated: %t\n", event.Data, event, event.Outdated)
|
||||
})
|
||||
for i := 0; i < 2; i++ {
|
||||
fmt.Println("asdfsafasfasfasfasfasf")
|
||||
m.CallA("playlist.update", i)
|
||||
fmt.Println("asdfsafasfasfasfasfasf")
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package event
|
||||
|
||||
var MAX_QUEUE_SIZE = 128
|
||||
var MAX_WORKER_SIZE = 16
|
||||
|
||||
var MainManager *Manager
|
||||
|
||||
func init() {
|
||||
MainManager = NewManger(MAX_QUEUE_SIZE, MAX_WORKER_SIZE)
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package logger
|
||||
|
||||
//var Logger *logrus.Logger
|
||||
//
|
||||
//func init() {
|
||||
// Logger = logrus.New()
|
||||
// Logger.SetLevel(config.Log.Level)
|
||||
// Logger.SetFormatter(&nested.Formatter{
|
||||
// FieldsOrder: []string{"Module"},
|
||||
// HideKeys: true,
|
||||
// NoColors: true,
|
||||
// })
|
||||
// //_ = os.Truncate(config.Log.Path, 0)
|
||||
// file, err := os.OpenFile(config.Log.Path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
// if err == nil {
|
||||
// Logger.Out = io.MultiWriter(file, os.Stdout)
|
||||
// } else {
|
||||
// Logger.Info("Failed to log to file, using default stdout")
|
||||
// }
|
||||
// if config.Log.RedirectStderr {
|
||||
// Logger.Info("panic/stderr redirect to log file")
|
||||
// if _, err = paniclog.RedirectStderr(file); err != nil {
|
||||
// Logger.Infof("Failed to redirect stderr to to file: %s", err)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -1,7 +0,0 @@
|
||||
package util
|
||||
|
||||
import "fmt"
|
||||
|
||||
func FormatTime(time int) string {
|
||||
return fmt.Sprintf("%01d:%02d", time/60, time%60)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import "AynaLivePlayer/core/model"
|
||||
|
||||
type IApplication interface {
|
||||
Version() model.VersionInfo
|
||||
LatestVersion() model.VersionInfo
|
||||
CheckUpdate() error
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/common/event"
|
||||
"AynaLivePlayer/core/model"
|
||||
)
|
||||
|
||||
type LiveClientCtor func(id string, ev *event.Manager, log ILogger) (LiveClient, error)
|
||||
|
||||
type LiveClient interface {
|
||||
ClientName() string
|
||||
RoomName() string
|
||||
Connect() bool
|
||||
Disconnect() bool
|
||||
Status() bool
|
||||
EventManager() *event.Manager
|
||||
}
|
||||
|
||||
type ILiveRoom interface {
|
||||
Client() LiveClient
|
||||
Model() *model.LiveRoom // should return mutable model (not a copy)
|
||||
Identifier() string
|
||||
DisplayName() string
|
||||
Status() bool
|
||||
EventManager() *event.Manager
|
||||
}
|
||||
|
||||
type LiveRoomExecutor interface {
|
||||
Match(command string) bool
|
||||
Execute(command string, args []string, danmu *model.DanmuMessage)
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package adapter
|
||||
|
||||
type LogLevel uint32
|
||||
|
||||
const (
|
||||
LogLevelError LogLevel = iota
|
||||
LogLevelWarn
|
||||
LogLevelInfo
|
||||
LogLevelDebug
|
||||
)
|
||||
|
||||
type ILogger interface {
|
||||
Debug(args ...interface{})
|
||||
Debugf(format string, args ...interface{})
|
||||
Info(args ...interface{})
|
||||
Infof(format string, args ...interface{})
|
||||
Warn(args ...interface{})
|
||||
Warnf(format string, args ...interface{})
|
||||
Error(args ...interface{})
|
||||
Errorf(format string, args ...interface{})
|
||||
WithModule(prefix string) ILogger
|
||||
SetLogLevel(level LogLevel)
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/common/event"
|
||||
"AynaLivePlayer/core/model"
|
||||
)
|
||||
|
||||
type PlayerCtor func(ev *event.Manager, log ILogger) IPlayer
|
||||
|
||||
type IPlayer interface {
|
||||
// Start the player
|
||||
Start()
|
||||
// Stop the player
|
||||
Stop()
|
||||
// Play play a media
|
||||
Play(media *model.Media) error
|
||||
// GetPlaying get playing media
|
||||
// if player is idle, return nil
|
||||
GetPlaying() *model.Media
|
||||
// IsPaused return true if player is paused
|
||||
IsPaused() bool
|
||||
// Pause pause player
|
||||
Pause() error
|
||||
// Unpause unpause player
|
||||
Unpause() error
|
||||
// SetVolume set volume
|
||||
SetVolume(volume float64) error
|
||||
// IsIdle return true if player is playing anything
|
||||
IsIdle() bool
|
||||
// Seek to position, if absolute is true, position is absolute time, otherwise position is relative time
|
||||
Seek(position float64, absolute bool) error
|
||||
// SetWindowHandle set window handle for video output
|
||||
SetWindowHandle(handle uintptr) error
|
||||
// ObserveProperty observe player property change
|
||||
ObserveProperty(property model.PlayerProperty, name string, handler event.HandlerFunc) error
|
||||
// GetAudioDeviceList get audio device list
|
||||
GetAudioDeviceList() ([]model.AudioDevice, error)
|
||||
// SetAudioDevice set audio device
|
||||
SetAudioDevice(device string) error
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/common/event"
|
||||
"AynaLivePlayer/core/model"
|
||||
)
|
||||
|
||||
type IPlaylist interface {
|
||||
Identifier() string // must unique for each playlist
|
||||
Model() *model.Playlist // mutable model (not a copy)
|
||||
EventManager() *event.Manager
|
||||
DisplayName() string
|
||||
Size() int
|
||||
Get(index int) *model.Media
|
||||
Pop() *model.Media
|
||||
Replace(medias []*model.Media)
|
||||
Push(media *model.Media)
|
||||
Insert(index int, media *model.Media)
|
||||
Delete(index int) *model.Media
|
||||
Move(src int, dst int)
|
||||
Next() *model.Media
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import "AynaLivePlayer/core/model"
|
||||
|
||||
type MediaProviderConfig map[string]string
|
||||
type MediaProviderCtor func(config MediaProviderConfig) MediaProvider
|
||||
|
||||
type MediaProvider interface {
|
||||
GetName() string
|
||||
MatchMedia(keyword string) *model.Media
|
||||
GetPlaylist(playlist *model.Meta) ([]*model.Media, error)
|
||||
FormatPlaylistUrl(uri string) string
|
||||
Search(keyword string) ([]*model.Media, error)
|
||||
UpdateMedia(media *model.Media) error
|
||||
UpdateMediaUrl(media *model.Media) error
|
||||
UpdateMediaLyric(media *model.Media) error
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/common/event"
|
||||
"AynaLivePlayer/core/model"
|
||||
)
|
||||
|
||||
// IControlBridge is the interface for all controller and
|
||||
// all system use cases.
|
||||
type IControlBridge interface {
|
||||
App() IApplication
|
||||
LiveRooms() ILiveRoomController
|
||||
PlayControl() IPlayController
|
||||
Playlists() IPlaylistController
|
||||
Provider() IProviderController
|
||||
Plugin() IPluginController
|
||||
LoadPlugins(plugins ...Plugin)
|
||||
Logger() ILogger
|
||||
CloseAndSave()
|
||||
}
|
||||
|
||||
type ILiveRoomController interface {
|
||||
Size() int
|
||||
Get(index int) ILiveRoom
|
||||
GetRoomStatus(index int) bool
|
||||
Connect(index int) error
|
||||
Disconnect(index int) error
|
||||
AddRoom(clientName, roomId string) (*model.LiveRoom, error)
|
||||
DeleteRoom(index int) error
|
||||
AddDanmuCommand(executor LiveRoomExecutor)
|
||||
GetAllClientNames() []string
|
||||
}
|
||||
|
||||
type IProviderController interface {
|
||||
GetPriority() []string
|
||||
PrepareMedia(media *model.Media) error
|
||||
MediaMatch(keyword string) *model.Media
|
||||
Search(keyword string) ([]*model.Media, error)
|
||||
SearchWithProvider(keyword string, provider string) ([]*model.Media, error)
|
||||
PreparePlaylist(playlist IPlaylist) error
|
||||
}
|
||||
|
||||
type IPluginController interface {
|
||||
LoadPlugin(plugin Plugin)
|
||||
LoadPlugins(plugins ...Plugin)
|
||||
ClosePlugins()
|
||||
}
|
||||
|
||||
type IPlaylistController interface {
|
||||
Size() int
|
||||
GetHistory() IPlaylist
|
||||
AddToHistory(media *model.Media)
|
||||
GetDefault() IPlaylist
|
||||
GetCurrent() IPlaylist
|
||||
Get(index int) IPlaylist
|
||||
Add(pname string, uri string) (IPlaylist, error)
|
||||
Remove(index int) (IPlaylist, error)
|
||||
SetDefault(index int) error
|
||||
PreparePlaylistByIndex(index int) error
|
||||
}
|
||||
|
||||
type IPlayControlConfig struct {
|
||||
SkipPlaylist bool
|
||||
AutoNextWhenFail bool
|
||||
}
|
||||
|
||||
type IPlayController interface {
|
||||
EventManager() *event.Manager
|
||||
GetPlaying() *model.Media
|
||||
GetPlayer() IPlayer
|
||||
PlayNext()
|
||||
Play(media *model.Media) error
|
||||
Add(keyword string, user interface{})
|
||||
AddWithProvider(keyword string, provider string, user interface{})
|
||||
Seek(position float64, absolute bool)
|
||||
Toggle() bool
|
||||
SetVolume(volume float64)
|
||||
Destroy()
|
||||
GetCurrentAudioDevice() string
|
||||
GetAudioDevices() []model.AudioDevice
|
||||
SetAudioDevice(device string)
|
||||
GetLyric() ILyricLoader
|
||||
Config() *IPlayControlConfig
|
||||
}
|
||||
|
||||
type ILyricLoader interface {
|
||||
EventManager() *event.Manager
|
||||
Get() *model.Lyric
|
||||
Reload(lyric string)
|
||||
Update(time float64)
|
||||
}
|
||||
@@ -1,54 +1,56 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/common/event"
|
||||
"AynaLivePlayer/core/model"
|
||||
)
|
||||
//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 (
|
||||
EventPlay event.EventId = "player.play"
|
||||
EventPlayed event.EventId = "player.played"
|
||||
EventPlaylistPreInsert event.EventId = "playlist.insert.pre"
|
||||
EventPlaylistInsert event.EventId = "playlist.insert.after"
|
||||
EventPlaylistUpdate event.EventId = "playlist.update"
|
||||
EventLyricUpdate event.EventId = "lyric.update"
|
||||
EventLyricReload event.EventId = "lyric.reload"
|
||||
)
|
||||
const ErrorUpdate = "update.error"
|
||||
|
||||
func EventPlayerPropertyUpdate(property model.PlayerProperty) event.EventId {
|
||||
return event.EventId("player.property.update." + string(property))
|
||||
type ErrorUpdateEvent struct {
|
||||
Error error
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
//
|
||||
//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 UpdateLiveRoomStatusData struct {
|
||||
// RoomTitle string
|
||||
// Status bool
|
||||
//}
|
||||
|
||||
25
core/events/events.go
Normal file
25
core/events/events.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package events
|
||||
|
||||
/*
|
||||
# events package
|
||||
|
||||
events package contains all events used in application.
|
||||
|
||||
in theory. all interaction should use events package.
|
||||
|
||||
the events are dispatched using eventbus package.
|
||||
|
||||
Here are some major events
|
||||
|
||||
- cmd: call cmd
|
||||
- reply: call reply
|
||||
- update: information updating event. usually issued by internal controller and broadcast to all channel
|
||||
|
||||
|
||||
naming convention
|
||||
|
||||
- cmd: 'cmd.event.id.'
|
||||
- reply: 'reply.same.same.cmd.id'
|
||||
- update: 'update.event.id'
|
||||
|
||||
*/
|
||||
7
core/events/gui.go
Normal file
7
core/events/gui.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package events
|
||||
|
||||
const GUISetPlayerWindowOpenCmd = "cmd.gui.player_window.op"
|
||||
|
||||
type GUISetPlayerWindowOpenCmdEvent struct {
|
||||
SetOpen bool
|
||||
}
|
||||
@@ -1,16 +1,64 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/common/event"
|
||||
"AynaLivePlayer/core/adapter"
|
||||
"AynaLivePlayer/core/model"
|
||||
liveroomsdk "github.com/AynaLivePlayer/liveroom-sdk"
|
||||
)
|
||||
|
||||
const (
|
||||
LiveRoomStatusChange event.EventId = "liveclient.status.change"
|
||||
LiveRoomMessageReceive event.EventId = "liveclient.message.receive"
|
||||
)
|
||||
const CmdLiveRoomAdd = "cmd.liveroom.add"
|
||||
|
||||
type StatusChangeEvent struct {
|
||||
Connected bool
|
||||
Client adapter.LiveClient
|
||||
type CmdLiveRoomAddData struct {
|
||||
Title string
|
||||
Provider string
|
||||
RoomKey string
|
||||
}
|
||||
|
||||
const CmdLiveRoomRemove = "cmd.liveroom.remove"
|
||||
|
||||
type CmdLiveRoomRemoveData struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
const CmdLiveRoomConfigChange = "cmd.liveroom.config.change"
|
||||
|
||||
type CmdLiveRoomConfigChangeData struct {
|
||||
Identifier string
|
||||
Config model.LiveRoomConfig
|
||||
}
|
||||
|
||||
const LiveRoomProviderUpdate = "update.liveroom.provider"
|
||||
|
||||
type LiveRoomProviderUpdateEvent struct {
|
||||
Providers []model.LiveRoomProviderInfo
|
||||
}
|
||||
|
||||
const UpdateLiveRoomRooms = "update.liveroom.rooms"
|
||||
|
||||
type UpdateLiveRoomRoomsData struct {
|
||||
Rooms []model.LiveRoom
|
||||
}
|
||||
|
||||
const UpdateLiveRoomStatus = "update.liveroom.status"
|
||||
|
||||
type UpdateLiveRoomStatusData struct {
|
||||
Room model.LiveRoom
|
||||
}
|
||||
|
||||
const CmdLiveRoomOperation = "cmd.liveroom.operation"
|
||||
|
||||
type CmdLiveRoomOperationData struct {
|
||||
Identifier string
|
||||
SetConnect bool // connect or disconnect
|
||||
}
|
||||
|
||||
const ReplyLiveRoomOperation = "reply.liveroom.operation"
|
||||
|
||||
type ReplyLiveRoomOperationData struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
const LiveRoomMessageReceive = "update.liveroom.message"
|
||||
|
||||
type LiveRoomMessageReceiveEvent struct {
|
||||
Message *liveroomsdk.Message
|
||||
}
|
||||
|
||||
78
core/events/mapping.go
Normal file
78
core/events/mapping.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/model"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
var EventsMapping = map[string]any{
|
||||
CmdLiveRoomAdd: CmdLiveRoomAddData{},
|
||||
LiveRoomProviderUpdate: LiveRoomProviderUpdateEvent{},
|
||||
CmdLiveRoomRemove: CmdLiveRoomRemoveData{},
|
||||
UpdateLiveRoomRooms: UpdateLiveRoomRoomsData{},
|
||||
UpdateLiveRoomStatus: UpdateLiveRoomStatusData{},
|
||||
CmdLiveRoomConfigChange: CmdLiveRoomConfigChangeData{},
|
||||
CmdLiveRoomOperation: CmdLiveRoomOperationData{},
|
||||
PlayerVolumeChangeCmd: PlayerVolumeChangeCmdEvent{},
|
||||
PlayerPlayCmd: PlayerPlayCmdEvent{},
|
||||
PlayerPlayErrorUpdate: PlayerPlayErrorUpdateEvent{},
|
||||
PlayerSeekCmd: PlayerSeekCmdEvent{},
|
||||
PlayerToggleCmd: PlayerToggleCmdEvent{},
|
||||
PlayerSetPauseCmd: PlayerSetPauseCmdEvent{},
|
||||
PlayerPlayNextCmd: PlayerPlayNextCmdEvent{},
|
||||
CmdGetCurrentLyric: CmdGetCurrentLyricData{},
|
||||
UpdateCurrentLyric: UpdateCurrentLyricData{},
|
||||
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{},
|
||||
CmdMiaosicSearch: CmdMiaosicSearchData{},
|
||||
ReplyMiaosicSearch: ReplyMiaosicSearchData{},
|
||||
GUISetPlayerWindowOpenCmd: GUISetPlayerWindowOpenCmdEvent{},
|
||||
}
|
||||
|
||||
func init() {
|
||||
for _, v := range []model.PlaylistID{model.PlaylistIDSystem, model.PlaylistIDPlayer} {
|
||||
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
|
||||
}
|
||||
22
core/events/mapping_test.go
Normal file
22
core/events/mapping_test.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUnmarshalEventData(t *testing.T) {
|
||||
eventData := CmdLiveRoomAddData{
|
||||
Title: "test",
|
||||
Provider: "asdfasd",
|
||||
RoomKey: "asdfasdf",
|
||||
}
|
||||
data, err := json.Marshal(eventData)
|
||||
require.NoError(t, err)
|
||||
val, err := UnmarshalEventData(CmdLiveRoomAdd, data)
|
||||
require.NoError(t, err)
|
||||
resultData, ok := val.(CmdLiveRoomAddData)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, eventData, resultData)
|
||||
}
|
||||
57
core/events/miaosic.go
Normal file
57
core/events/miaosic.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package events
|
||||
|
||||
import "github.com/AynaLivePlayer/miaosic"
|
||||
|
||||
const CmdMiaosicGetMediaInfo = "cmd.miaosic.getMediaInfo"
|
||||
|
||||
type CmdMiaosicGetMediaInfoData struct {
|
||||
Meta miaosic.MetaData `json:"meta"`
|
||||
}
|
||||
|
||||
const ReplyMiaosicGetMediaInfo = "reply.miaosic.getMediaInfo"
|
||||
|
||||
type ReplyMiaosicGetMediaInfoData struct {
|
||||
Info miaosic.MediaInfo `json:"info"`
|
||||
Error error `json:"error"`
|
||||
}
|
||||
|
||||
const CmdMiaosicGetMediaUrl = "cmd.miaosic.getMediaUrl"
|
||||
|
||||
type CmdMiaosicGetMediaUrlData struct {
|
||||
Meta miaosic.MetaData `json:"meta"`
|
||||
Quality miaosic.Quality `json:"quality"`
|
||||
}
|
||||
|
||||
const ReplyMiaosicGetMediaUrl = "reply.miaosic.getMediaUrl"
|
||||
|
||||
type ReplyMiaosicGetMediaUrlData struct {
|
||||
Urls []miaosic.MediaUrl `json:"urls"`
|
||||
Error error `json:"error"`
|
||||
}
|
||||
|
||||
const CmdMiaosicQrLogin = "cmd.miaosic.qrLogin"
|
||||
|
||||
type CmdMiaosicQrLoginData struct {
|
||||
Provider string `json:"provider"`
|
||||
}
|
||||
|
||||
const ReplyMiaosicQrLogin = "reply.miaosic.qrLogin"
|
||||
|
||||
type ReplyMiaosicQrLoginData struct {
|
||||
Session miaosic.QrLoginSession `json:"session"`
|
||||
Error error `json:"error"`
|
||||
}
|
||||
|
||||
const CmdMiaosicQrLoginVerify = "cmd.miaosic.qrLoginVerify"
|
||||
|
||||
type CmdMiaosicQrLoginVerifyData struct {
|
||||
Provider string `json:"provider"`
|
||||
Session miaosic.QrLoginSession `json:"session"`
|
||||
}
|
||||
|
||||
const ReplyMiaosicQrLoginVerify = "reply.miaosic.qrLoginVerify"
|
||||
|
||||
type ReplyMiaosicQrLoginVerifyData struct {
|
||||
Result miaosic.QrLoginResult `json:"result"`
|
||||
Error error `json:"error"`
|
||||
}
|
||||
49
core/events/player_control.go
Normal file
49
core/events/player_control.go
Normal 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 {
|
||||
}
|
||||
23
core/events/player_lyric.go
Normal file
23
core/events/player_lyric.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package events
|
||||
|
||||
import "github.com/AynaLivePlayer/miaosic"
|
||||
|
||||
const CmdGetCurrentLyric = "cmd.player.lyric.request"
|
||||
|
||||
type CmdGetCurrentLyricData struct {
|
||||
}
|
||||
|
||||
const UpdateCurrentLyric = "update.player.lyric.reload"
|
||||
|
||||
type UpdateCurrentLyricData 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
|
||||
}
|
||||
46
core/events/player_property.go
Normal file
46
core/events/player_property.go
Normal 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
|
||||
}
|
||||
22
core/events/player_videoplayer.go
Normal file
22
core/events/player_videoplayer.go
Normal 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
88
core/events/playlist.go
Normal 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
54
core/events/playlists.go
Normal 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
7
core/events/provider.go
Normal 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
18
core/events/search.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/model"
|
||||
)
|
||||
|
||||
const CmdMiaosicSearch = "cmd.search"
|
||||
|
||||
type CmdMiaosicSearchData struct {
|
||||
Keyword string
|
||||
Provider string
|
||||
}
|
||||
|
||||
const ReplyMiaosicSearch = "update.search_result"
|
||||
|
||||
type ReplyMiaosicSearchData struct {
|
||||
Medias []model.Media
|
||||
}
|
||||
15
core/events/updater.go
Normal file
15
core/events/updater.go
Normal 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
|
||||
}
|
||||
@@ -1,40 +1,26 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
import "github.com/AynaLivePlayer/liveroom-sdk"
|
||||
|
||||
type LiveRoomConfig struct {
|
||||
AutoConnect bool `json:"auto_connect"`
|
||||
}
|
||||
|
||||
type LiveRoom struct {
|
||||
ClientName string
|
||||
ID string
|
||||
Title string
|
||||
AutoConnect bool
|
||||
AutoReconnect bool
|
||||
LiveRoom liveroom.LiveRoom `json:"live_room"`
|
||||
Config LiveRoomConfig `json:"config"`
|
||||
Title string `json:"title"`
|
||||
Status bool `json:"status"`
|
||||
}
|
||||
|
||||
func (r *LiveRoom) String() string {
|
||||
return fmt.Sprintf("<LiveRooms %s:%s>", r.ClientName, r.ID)
|
||||
func (r *LiveRoom) DisplayName() string {
|
||||
if r.Title != "" {
|
||||
return r.Title
|
||||
}
|
||||
return r.LiveRoom.Identifier()
|
||||
}
|
||||
|
||||
func (r *LiveRoom) Identifier() string {
|
||||
return fmt.Sprintf("%s_%s", r.ClientName, r.ID)
|
||||
}
|
||||
|
||||
type UserMedal struct {
|
||||
Name string
|
||||
Level int
|
||||
RoomID string
|
||||
}
|
||||
|
||||
type DanmuUser struct {
|
||||
Uid string
|
||||
Username string
|
||||
Medal UserMedal
|
||||
Admin bool
|
||||
Privilege int
|
||||
}
|
||||
|
||||
type DanmuMessage struct {
|
||||
User DanmuUser
|
||||
Message string
|
||||
type LiveRoomProviderInfo struct {
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/spf13/cast"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var timeTagRegex = regexp.MustCompile("\\[[0-9]+:[0-9]+(\\.[0-9]+)?\\]")
|
||||
|
||||
type LyricLine struct {
|
||||
Time float64 // in seconds
|
||||
Lyric string
|
||||
Translation string
|
||||
}
|
||||
|
||||
type LyricContext struct {
|
||||
Now *LyricLine
|
||||
Index int
|
||||
Total int
|
||||
Prev []*LyricLine
|
||||
Next []*LyricLine
|
||||
}
|
||||
|
||||
type Lyric struct {
|
||||
Lyrics []*LyricLine
|
||||
}
|
||||
|
||||
func LoadLyric(lyric string) *Lyric {
|
||||
tmp := make(map[float64]*LyricLine)
|
||||
times := make([]float64, 0)
|
||||
for _, line := range strings.Split(lyric, "\n") {
|
||||
lrc := timeTagRegex.ReplaceAllString(line, "")
|
||||
if len(lrc) > 0 && lrc[len(lrc)-1] == '\r' {
|
||||
lrc = lrc[:len(lrc)-1]
|
||||
}
|
||||
for _, time := range timeTagRegex.FindAllString(line, -1) {
|
||||
ts := strings.Split(time[1:len(time)-1], ":")
|
||||
t := cast.ToFloat64(ts[0])*60 + cast.ToFloat64(ts[1])
|
||||
times = append(times, t)
|
||||
tmp[t] = &LyricLine{
|
||||
Time: t,
|
||||
Lyric: lrc,
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Float64s(times)
|
||||
lrcs := make([]*LyricLine, len(times))
|
||||
for index, time := range times {
|
||||
lrcs[index] = tmp[time]
|
||||
}
|
||||
if len(lrcs) == 0 {
|
||||
lrcs = append(lrcs, &LyricLine{Time: 0, Lyric: ""})
|
||||
}
|
||||
lrcs = append(lrcs, &LyricLine{
|
||||
Time: 99999999999,
|
||||
Lyric: "",
|
||||
})
|
||||
return &Lyric{Lyrics: lrcs}
|
||||
}
|
||||
|
||||
func (l *Lyric) findIndexV1(time float64) int {
|
||||
for i := 0; i < len(l.Lyrics)-1; i++ {
|
||||
if l.Lyrics[i].Time <= time && time < l.Lyrics[i+1].Time {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (l *Lyric) findIndex(time float64) int {
|
||||
start := 0
|
||||
end := len(l.Lyrics) - 1
|
||||
mid := (start + end) / 2
|
||||
for start < end {
|
||||
if l.Lyrics[mid].Time <= time && time < l.Lyrics[mid+1].Time {
|
||||
return mid
|
||||
}
|
||||
if l.Lyrics[mid].Time > time {
|
||||
end = mid
|
||||
} else {
|
||||
start = mid
|
||||
}
|
||||
mid = (start + end) / 2
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (l *Lyric) Find(time float64) *LyricLine {
|
||||
idx := l.findIndex(time)
|
||||
if idx == -1 {
|
||||
return nil
|
||||
}
|
||||
return l.Lyrics[idx]
|
||||
}
|
||||
|
||||
func (l *Lyric) FindContext(time float64, prev int, next int) *LyricContext {
|
||||
prev = -prev
|
||||
idx := l.findIndex(time)
|
||||
if idx == -1 {
|
||||
return nil
|
||||
}
|
||||
if (idx + prev) < 0 {
|
||||
prev = -idx
|
||||
}
|
||||
if (idx + 1 + next) > len(l.Lyrics) {
|
||||
next = len(l.Lyrics) - idx - 1
|
||||
}
|
||||
return &LyricContext{
|
||||
Now: l.Lyrics[idx],
|
||||
Index: idx,
|
||||
Total: len(l.Lyrics),
|
||||
Prev: l.Lyrics[idx+prev : idx],
|
||||
Next: l.Lyrics[idx+1 : idx+1+next],
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/magiconair/properties/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var testLyric = "[ti:双截棍]\n[ar:周杰伦]\n[al:范特西]\n[00:03.85]双截棍\n[00:07.14]\n[00:30.13]岩烧店的烟味弥漫隔壁是国术馆\n[00:32.57]店里面的妈妈桑茶道有三段\n[00:34.61]教拳脚武术的老板练铁沙掌耍杨家枪\n[00:37.34]硬底子功夫最擅长还会金钟罩铁步衫\n[00:39.67]他们儿子我习惯从小就耳濡目染\n[00:41.96]什么刀枪跟棍棒我都耍的有模有样\n[00:44.22]什么兵器最喜欢双截棍柔中带刚\n[00:46.73]想要去河南嵩山学少林跟武当\n[00:49.24]干什么(客)干什么(客)呼吸吐纳心自在\n[00:51.28]干什么(客)干什么(客)气沉丹田手心开\n[00:53.44]干什么(客)干什么(客)日行千里系沙袋\n[00:56.13]飞檐走壁莫奇怪去去就来\n[00:58.35]一个马步向前一记左钩拳右钩拳\n[01:01.26]一句惹毛我的人有危险一再重演\n[01:04.02]一根我不抽的菸一放好多年它一直在身边\n[01:07.28]干什么(客)干什么(客)我打开任督二脉\n[01:10.27]干什么(客)干什么(客)东亚病夫的招牌\n[01:12.75]干什么(客)干什么(客)已被我一脚踢开\n[02:32.62][01:54.69][01:15.40]快使用双截棍哼哼哈兮\n[02:34.52][01:56.63][01:18.40]快使用双截棍哼哼哈兮\n[02:36.88][01:58.98][01:20.71]习武之人切记仁者无敌\n[02:39.45][02:01.66][01:23.27]是谁在练太极风生水起\n[02:41.97][02:03.93][01:25.74]快使用双截棍哼哼哈兮\n[02:44.42][02:06.11][01:27.75]快使用双截棍哼哼哈兮\n[02:47.01][02:08.54][01:30.13]如果我有轻功飞檐走壁\n[02:49.36][02:11.03][01:32.67]为人耿直不屈一身正气\n[02:53.81]快使用双截棍哼\n[02:56.30]我用手刀防御哼\n[02:58.52]漂亮的回旋踢\n[02:59.52]"
|
||||
|
||||
func TestLyric(t *testing.T) {
|
||||
lryic := LoadLyric(testLyric)
|
||||
for _, lrc := range lryic.Lyrics {
|
||||
fmt.Println(lrc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLyricFind(t *testing.T) {
|
||||
lryic := LoadLyric(testLyric)
|
||||
fmt.Println(lryic.Find(90.4))
|
||||
for _, l := range lryic.FindContext(90.4, -2, 2).Next {
|
||||
fmt.Println(l)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLyricFindV2(t *testing.T) {
|
||||
lryic := LoadLyric(testLyric)
|
||||
for i := 0.0; i < 170; i += 0.01 {
|
||||
assert.Equal(t, lryic.Find(i), lryic.Find(i))
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,37 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/AynaLivePlayer/liveroom-sdk"
|
||||
"github.com/AynaLivePlayer/miaosic"
|
||||
)
|
||||
|
||||
type Picture struct {
|
||||
Url string
|
||||
Data []byte
|
||||
type User struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (p Picture) Exists() bool {
|
||||
return p.Url != "" || p.Data != nil
|
||||
}
|
||||
var PlaylistUser = User{Name: "Playlists"}
|
||||
var SystemUser = User{Name: "System"}
|
||||
|
||||
type Media struct {
|
||||
Title string
|
||||
Artist string
|
||||
Cover Picture
|
||||
Album string
|
||||
Lyric string
|
||||
Url string
|
||||
Header map[string]string
|
||||
User interface{}
|
||||
Meta interface{}
|
||||
Info miaosic.MediaInfo
|
||||
User interface{}
|
||||
}
|
||||
|
||||
func (m *Media) ToUser() *User {
|
||||
if u, ok := m.User.(*User); ok {
|
||||
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}
|
||||
return User{Name: m.DanmuUser().Username}
|
||||
}
|
||||
|
||||
func (m *Media) DanmuUser() *DanmuUser {
|
||||
if u, ok := m.User.(*DanmuUser); ok {
|
||||
func (m *Media) DanmuUser() liveroom.User {
|
||||
if u, ok := m.User.(liveroom.User); ok {
|
||||
return u
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Media) Copy() *Media {
|
||||
newMedia := &Media{}
|
||||
copier.Copy(newMedia, m)
|
||||
return newMedia
|
||||
return liveroom.User{}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user