227 Commits

Author SHA1 Message Date
tomasklaen
61c259b137 feat: default_directory option now supports {drives} on windows
On unix, this will switch to `/`.

closes #953
2024-08-28 20:05:21 +02:00
tomasklaen
e975429086 feat: display ignored bindings in keybinds menu
ref #901
2024-08-28 18:30:19 +02:00
tomasklaen
5f701436d1 feat: top_bar_flash_on option now supports chapter as an event
ref #478
2024-08-28 18:14:55 +02:00
Tomas Klaen
7f054a45f8 feat: menu lifecycle and actions API (#936) 2024-08-28 17:37:53 +02:00
tomasklaen
a0a608d451 feat: window controls now respect opacity.controls config
closes #949
2024-08-19 09:48:34 +02:00
dyphire
8346db433f fix: always read romanizations for configured languages (#948)
When I originally added this feature, I overlooked that this was actually different from UI localization.
It has different usage scenarios: UI main language and romanization files can be separated
2024-08-13 10:41:17 +02:00
Tomas Klaen
fe0d394435 feat: set-button API for scripts to add custom control bar buttons (#938)
* feat: `set-button` API for scripts to add custom control bar buttons

Adds `set-button <name> <data_json>` message external scripts can send to add or update buttons whose state, look, and click action is defined by `data_json`.

Users can then add this button into their controls bar with `button:<name>` syntax.

closes #935
2024-07-29 10:48:51 +02:00
christoph-heinrich
9fa7220da4 fix: use primary_click for chapter indicators (#932)
Listening to `primary_down`, but not listening to the `primary_up` event
leads to that `up` event getting forwarded. Listening to `primary_click`
instead avoids that problem.

Seeking chapters now happens on button up instead of button down, but
that shouldn't be a problem for anyone and makes their behavior
consistent with other clickable UI elements.

ref. https://github.com/tomasklaen/uosc/discussions/931
2024-07-12 22:44:48 +02:00
christoph-heinrich
c660db0743 feat: use loadlist when opening playlists (#929)
Opening a playlist with loadfile can lead to problems like autoload.lua
populating the playlist with other files that were in the same directory
as the playlist file.

To avoid that the command loadlist can be used to avoid having mpv treat
the file like any other, just to notice during demuxing that it is a
playlist.
2024-07-09 09:29:00 +02:00
christoph-heinrich
3bf774bf54 feat: show playlist files in the file navigation menu (#927)
So far the file navigation menu would only show media files and
directories, but opening playlist files via that menu is a valid use
case as well.

From a quick look at demux_playlist.c and a bit of testing, those seem
to be the only playlist formats mpv supports.
mpv can also open .txt files as playlists, but they need to be provided
via the --playlist=<path> argument and thus don't work here.

ref. https://github.com/tomasklaen/uosc/discussions/926
2024-07-01 23:07:58 +02:00
tomasklaen
1c38d4d3a7 fix: timeline_step wasn't being disabled when set to 0
closes #910
2024-06-03 09:22:48 +02:00
tomasklaen
08df4aa944 fix: menu item separators were being ignored
ref #901
2024-05-27 10:12:53 +02:00
christoph-heinrich
b01c6df787 fix: buffered_time_threshold check (#907)
a7136337c8 fixed the shown duration,
but the threshold check wasn't adjusted and therefore still didn't behave
the same as before 00737e1598.
2024-05-20 23:06:25 +02:00
tomasklaen
43dcb51eb9 feat: added additional selected menu item indicator
closes #853
2024-05-09 11:59:06 +02:00
tomasklaen
0e60272db1 tweak: platform agnostic window controls design 2024-05-08 11:05:13 +02:00
tomasklaen
2940352fad fix: keybinds menu not listing mpv-menu-plugin keyless syntax commands
ref #901
2024-05-07 17:13:10 +02:00
tomasklaen
d4b1343609 fix: keybinds menu list not skipping duplicate, disabled, and ignore keybinds
ref #901
2024-05-07 16:55:15 +02:00
dyphire
89360096c6 fix: display proper capitalization and selected index in track menus (#902)
mpv upstream commit mpv-player/mpv@ab3b174 has introduced support for BCP 47 language tags,
and now track lang has uppercase and lowercase content.

Also avoids showing the selection status of secondary subtitle in the subtitle menu at the same time.
2024-05-06 11:34:26 +02:00
tomasklaen
e15523a56e feat: keybinds menu now also includes keyless or overwritten bindings from input.conf
closes #901
2024-05-06 11:21:16 +02:00
tomasklaen
4c64fe9d52 fix: flash out animations always starting from max instead of current visibility value
closes #900
2024-05-06 10:12:57 +02:00
christoph-heinrich
7f03046223 feat: support aligning window controls to the left (#894) 2024-05-04 22:07:34 +02:00
christoph-heinrich
a7136337c8 fix: scale cache duration with speed again (#899)
fixes 00737e1598
2024-05-04 22:06:46 +02:00
christoph-heinrich
56b62bb0b2 tweak: same spacing between all top bar elements (#893)
There is no reason why that space should be any different to the one
between the playlist and the title
2024-05-01 16:21:43 +02:00
christoph-heinrich
00737e1598 refactor: use demuxer-cache-state/cache-duration property (#889)
It's used by both osc.lua and stats.lua and gives a more accurate
estimation of how much media is cached.
2024-04-23 14:41:13 +02:00
tomasklaen
e6a5fd981d fix: cycle button controls not working properly on floating point values like speed
closes #881
2024-04-07 13:27:46 +02:00
tomasklaen
6fa34c31d0 fix: chapter remaining time scaled by speed even when destination_time is set to time-remaining 2024-03-13 11:01:12 +01:00
tomasklaen
581adf4c5b fix: remaining chapter time in top bar wasn't accounting for speed
closes #873
2024-03-12 14:33:52 +01:00
christoph-heinrich
db985f1cbb fix: human time formats not accounting for speed < 1 (#871) 2024-03-11 09:35:16 +01:00
christoph-heinrich
48e0100727 fix: redraw when toggling the title in the top bar (#870) 2024-03-04 15:00:14 +01:00
christoph-heinrich
3675ed47c3 fix: refine text_width check (#869)
The option did the opposite of what it was supposed to.
2024-03-04 14:59:48 +01:00
tomasklaen
35699d3004 fix: menu crash after ctrl+backspace with only one character in input field
closes #847
2024-02-20 16:17:57 +01:00
tomasklaen
2370cb4e1d fix: disabling chapter indicators now disables chapter tooltips as well
resolves #849
2024-02-20 09:17:48 +01:00
tomasklaen
60dc19713e 5.2.0 2024-02-06 10:51:22 +01:00
tomasklaen
777ed2bb40 fix: keybindings and download-subtitles menus were not using palette search style 2024-02-06 10:45:45 +01:00
Tomas Klaen
4a4d0560b7 fix: click threshold not working when any clickable element is visible (#831)
This moves click threshold handling to cursor zones, which have been reworked a bit.

Resolves #820
2024-02-06 10:18:57 +01:00
tomasklaen
dc73278d65 feat: remaining chapter time in topbar
closes #839
2024-01-28 13:15:24 +01:00
christoph-heinrich
861f121c83 fix: don't scroll to top on menu update during search (#841) 2024-01-27 10:04:42 +01:00
tomasklaen
2eccc1bb06 fix: incorrect top bar state after re-enabling
closes #838
2024-01-20 21:48:22 +01:00
christoph-heinrich
63aba054ea fix: timestamp zero representation caching (#836)
Caching the zero represenation sounds like a good idea, until the
precision gets changed and then e.g. 01:40:23.13 returns -00:00:00.0
because it has the same amount of characters, messing up our width
estimation and pseudo monospace thingy.

Clearing the cache on options change avoids such conflicts.

Fixes #834
2024-01-20 21:26:07 +01:00
christoph-heinrich
0f970b5d8e feat: add items to playlist from files menu when holding ctrl (#822)
Closes #821
2024-01-08 19:18:43 +01:00
tomasklaen
efd6a55b20 feat: added flash-progress command
closes #828
2024-01-04 12:18:30 +01:00
Tomas Klaen
0d7825a4ad feat!: refine option to improve features at a cost of some performance (#816)
Replaces option `text_width_estimation`, which is now one of its available flags:

```
refine=text_width
```
2023-12-28 13:05:59 +01:00
dyphire
b36cefed88 feat: romanization support for search characters (#797) 2023-12-08 18:05:20 +01:00
dyphire
d01eb25c4d feat: more accurate file sorting on windows (#798) 2023-12-08 17:59:04 +01:00
tomasklaen
cef5694f96 fix: no window border on new mpv windows versions
There's a lot of `title-bar` interaction weirdness on windows. When disabling it on newer mpv versions, one version hides borders, other doesn't. So let's just pretend this setting does what it's supposed to and what everyone would expect, and only hides the title bar without touching the border. People can always disable uosc's window border with other options if they need to.
2023-12-08 17:38:50 +01:00
nicoo
9a02a60684 feat: support overriding ziggy path with MPV_UOSC_ZIGGY environment variable (#814)
This is useful when running on a platform builds of `ziggy` aren't provided for
... or when using a distro-provided build.

The idea for this patch originated in NixOS/nixpkgs#270962,
as it needed a way to point `uosc` at the nix-managed build of `ziggy`.
2023-12-07 11:03:21 +01:00
nicoo
8cfcdcfdec docs: fix broken link to uosc.conf (#813) 2023-12-07 10:44:37 +01:00
christoph-heinrich
79a77b1678 fix: couldn't disable buffering_indicator due to incorrect ID (#809)
The element id didn't match the id in the constructors table or the
description in the config. Consequently it was possible to disable the
indicator at startup, but once it was instantiated it wasn't possible to
remove it again.

Fixes #808
2023-12-07 09:56:04 +01:00
tomasklaen
472cbb1648 docs: add installation note about the antivirus issue 2023-12-06 23:33:06 +01:00
tomasklaen
bc9d20cb1a fix: toggle-ui not hiding elements when border=yes
closes #805
2023-12-06 23:05:42 +01:00
Sneakpeakcss
78061a7dc6 fix: show-in-directory issue opening paths containing a comma (win) (#800)
On Windows trying to open a path that contains a comma fails unless there's a space anywhere in the path, adding a trailing space to the end of it doesn't seem to bother explorer /select, and forces proper quotations.

Here's a bit more detail about this https://github.com/mpvnet-player/mpv.net/issues/580#issuecomment-1819365741
2023-12-06 22:36:14 +01:00
christoph-heinrich
96b57b259e fix: make maximize workaround windows specific (#795) 2023-11-15 20:14:10 +01:00
christoph-heinrich
210a1216fe fix: locale parsing related crash on some mpv builds (#794)
The # operator produces different results on luajit to the
interpreter for lists like `{1, nil, 3}`.
The interpreter gives a size of 3, while luajit says it's 1.

That caused `table_assign` and `itable_join` to behave differently depending
on the lua environment they're running in.

To get the total number of vararg arguments, `select('#', ...)`` can be
used, which doesn't stop counting when it encounters `nil`.

Any error return nil.
2023-11-13 18:58:43 +01:00
tomasklaen
28e968450f docs: clarified how to configure fetched languages in download-subtitles menu
closes #791
2023-11-12 19:13:31 +01:00
tomasklaen
55938d467a docs: updated faq with antivirus issue info 2023-11-10 10:02:55 +01:00
tomasklaen
1f4b7294b6 5.1.1 2023-11-10 09:30:52 +01:00
Tomas Klaen
c1828231ed chore: update screenshot in readme 2023-11-09 13:20:51 +01:00
tomasklaen
9cebb07fcb 5.1.0 2023-11-09 10:39:45 +01:00
tomasklaen
6d329f959b tweak: chapter indicator sizing 2023-11-09 10:39:22 +01:00
christoph-heinrich
38e68e1771 fix: scroll parent menus when opening menu by id (#780) 2023-11-09 09:33:25 +01:00
christoph-heinrich
a9c83bb73b tweak: always scroll to selected index on menu open (#779)
I don't think there is currently a use case for this, but maybe there
will be at some point and it doesn't hurt.

This would enable scrolling to some item on menu open even with
`mouse_nav` set, but currently any place that can open a menu with
`selected_index` set doesn't have `mouse_nav` set.
2023-11-09 09:31:56 +01:00
christoph-heinrich
8df5ea6efd fix: don't cause a fling in update_dimensions() (#778)
Opening a large menu with a selected item would not scroll
all the way to that selected item if there was a menu update during the
initial fling.
2023-11-09 09:30:30 +01:00
tomasklaen
0531659397 fix: controls couldn't cycle some mpv properties
closes #777
2023-11-07 15:10:39 +01:00
christoph-heinrich
ed42152bb8 fix: menu config with mpv.net (#776)
mpv.net v7 returns the whole input.conf content instead of the path for
the `input-conf` property.
2023-11-06 18:51:00 +01:00
tomasklaen
9c087efbb5 feat: added playlist_position to configurable opacity properties
closes #774
2023-11-06 09:14:04 +01:00
christoph-heinrich
c3e5bb2270 fix: only autohide UI when the cursor autohides (#771)
The cursor isn't allowed to autohide while hovering elements, so the UI
shouldn't autohide either. Otherwise this leads to the situation where
the UI autohides while e.g. hovering the timeline, and then the
cursor autohides afterwards because it's not hovering anything anymore.
2023-11-06 09:07:23 +01:00
christoph-heinrich
26d71a8630 fix: correctly update proximities on mouse enter and record position (#772)
Element:update_proximity() checks for cursor.hidden, which is why we have
to update that before updating the proximities.

Also it doesn't make sense to not record the first cursor position on
enter.
2023-11-05 11:37:32 +01:00
christoph-heinrich
a7ee37c1d7 refactor: remove check before adding 0.5 to cursor position (#770)
math.huge + 0.5 == math.huge
2023-11-05 11:24:25 +01:00
tomasklaen
650118ebbd docs: clarify how to control autohide timing 2023-11-04 19:58:08 +01:00
tomasklaen
d8044770ee tweak: bumped timeline cursor line opacity
ref #722
2023-11-04 19:45:58 +01:00
tomasklaen
bd068c9cd0 feat: added controls to configurable opacity properties
Changes background opacity of control buttons. Default is `0`.

ref #722
2023-11-04 19:38:15 +01:00
tomasklaen
c9826221d0 fix: autoload looping a file when only a single file in directory
closes #768
2023-11-04 19:05:14 +01:00
tomasklaen
37675d0f55 feat: allow pasting to start search
closes #767
2023-11-04 18:54:40 +01:00
tomasklaen
8d2dfc4b00 build: fixed packaging tool reporting total instead of compressed size 2023-11-04 18:41:26 +01:00
tomasklaen
a8f040af0d perf: speed up ziggy initialization by using less aggressive binary compression 2023-11-04 15:02:45 +01:00
tomasklaen
c18b374d32 docs: add faq 2023-11-04 15:01:06 +01:00
tomasklaen
de85bdebcf fix: loading same track twice would add a duplicate entry
Loading the same track now selects it if it wasn't already instead of loading a duplicate entry.
2023-11-04 13:02:14 +01:00
tomasklaen
81f402ad73 feat: support for paste in menus
This adds several paste related features:

- You can paste into search input box.
- You can paste a url or a path into track select menus (subtitles/audio/video) to load it.
- Menu API now accepts `on_paste` option, which works the same as `on_search`.

closes #765, ref #497
2023-11-04 12:22:23 +01:00
tomasklaen
4cdd6c585d fix: menu separators and scrollbar not scaling properly
closes #764
2023-11-03 15:06:13 +01:00
tomasklaen
76b5fbe911 fix: dynamic control shrinking would not subtract spacing
ref #740
2023-11-03 11:21:45 +01:00
christoph-heinrich
f1c41a57fa fix: keep selected index on menu update during search (#759) 2023-11-03 09:57:01 +01:00
christoph-heinrich
448d408d1a tweak: update scroll position when moving items like when navigating (#763) 2023-11-03 09:51:14 +01:00
dyphire
d2885dd279 fix: update simplified chinese translation (#748) 2023-11-01 18:55:39 +01:00
christoph-heinrich
0a1b42936e fix: update german translation (#747) 2023-11-01 18:55:19 +01:00
tomasklaen
88e15cac57 build: fix ziggy build on unix
ref #744
2023-11-01 18:11:31 +01:00
tomasklaen
b8280b0004 feat: control gaps now shrink when not enough space
closes #740
2023-11-01 14:49:24 +01:00
tomasklaen
16f61eb497 fix: manual pause indicator initializes as static when launched with --pause
Also refactored/simplified PauseIndicator a bit.

closes #743
2023-11-01 12:55:17 +01:00
tomasklaen
b862387bec tweak: tweaked subtitle downloader messages 2023-11-01 12:12:33 +01:00
tomasklaen
c173641ea5 chore: restructured repo to eliminate dist directory
As pointed out in #757, having scripts in `dist` would cause github's code indexing to ignore them.

This also updates packaging tool/scripts, which before wouldn't mark binaries placed inside `uosc.zip` as executable when built on windows. I can't believe there is no CLI tool on windows to do this and I had to write one in go...
2023-11-01 11:08:23 +01:00
christoph-heinrich
16e8ca3689 build: fixed unix build script syntax (#758) 2023-11-01 10:55:41 +01:00
Tomas Klaen
542f0db0fc feat: added menu for downloading subtitles from opensubtitles.com (#756)
New menu command `download-subtitles` which can also be opened by clicking on the **Download** option in the `subtitles` menu.

It uses a new `ziggy` binary which needs to be build with `tools/build ziggy` command.

ziggy will also hash the file and send the hash to Open Subtitles, so you can search even with empty query and if your file is known, you'll get results exactly for it.

The subtitle file is downloaded into the same directory as currently opened file, or `~~/subtitles` if URL is being played.
2023-10-31 17:21:23 +01:00
tomasklaen
13eb2dc137 feat(api): replaced menu palette option with search_style
`search_style` can be `'on_demand'` (current default), `'palette'`, or a new state `'disabled'` where menu can't be searched.
2023-10-31 16:46:15 +01:00
christoph-heinrich
6cbf0731f3 fix: timestamp shifting due to non monospace font (#752) 2023-10-31 15:39:37 +01:00
christoph-heinrich
bf7f970295 fix: allow a volume border of 0 (#749)
ref. https://github.com/tomasklaen/uosc/issues/722#issuecomment-1783866244
2023-10-30 18:16:32 +01:00
christoph-heinrich
4f091d30e7 fix: scale timeline timestaps border and margin (#751) 2023-10-30 18:13:52 +01:00
christoph-heinrich
28878e425a fix: add padding to menu width calculation (#755) 2023-10-30 18:12:46 +01:00
tomasklaen
55789e1d32 fix: menus crash when ctrl+backspace pressed while input is empty
Closes #745
2023-10-28 09:36:38 +02:00
Tomas Klaen
656ddcfb9b build: added building, packaging, and localization tools (#744)
Main feature here is the `intl` tool that allows people to quickly create or update localization files with simple:

```
tools/intl de
```

This will parse the code for localization strings, and compare it against `de` locale, removing unused, and assigning `null` to new strings.

We can also make it even easier for people by running:

```
tools/intl all
```

after every localization string change in the codebase, which will update all existing locales so people can just browse through their ones and fill in nulls.
2023-10-28 09:20:56 +02:00
christoph-heinrich
ed6dcbe085 fix: runtime updates for progress option (#739) 2023-10-25 23:23:34 +02:00
tomasklaen
6dc4c30eaf refactor: no dynamic localization calls 2023-10-25 19:51:45 +02:00
dyphire
66f035b332 fix: update simplified chinese translation (#734) 2023-10-24 00:00:15 +02:00
tomasklaen
18e81c27f4 tweak: bumped default border_radius from 2 to 4
Just trying to match other UIs in the world that use bigger radius (mac, win, gnome).
2023-10-23 23:56:36 +02:00
tomasklaen
96685b59fe tweak: menu separators restyled a bit
Basic separator opacity was clashing with actual separator too much, and the actual separator height with item heights.
2023-10-23 23:52:54 +02:00
tomasklaen
e5a1603263 feat: added menu_padding option
This also bumps default menu padding from previous `2` to `4`, and fixes some dimension calculations that weren't account for it. It also makes menu titles look more appropriately sized and text in them vertically centered.
2023-10-23 23:01:19 +02:00
christoph-heinrich
48f09ff1d6 fix: update german translation (#731) 2023-10-22 18:43:07 +02:00
tomasklaen
6e19bee955 fix: timeline chapter indicators not scaling appropriately 2023-10-22 15:33:16 +02:00
Taras Adamchuk
4123cbe6ee feat: added ukrainian translation (#728) 2023-10-22 14:57:07 +02:00
tomasklaen
7c5d4f13f3 refactor: more no dynamic localization calls
The purpose is to make it possible to create a localization file generating and updating script.
2023-10-22 14:35:02 +02:00
tomasklaen
36f80744fe refactor: no dynamic localization calls 2023-10-22 14:06:38 +02:00
tomasklaen
1f26c3ec23 feat: added play-pause control shorthand 2023-10-22 13:53:15 +02:00
christoph-heinrich
d49f385f7d fix: delete-file-prev not working as intended (#726) 2023-10-21 17:13:29 +02:00
christoph-heinrich
192c3efb16 fix: update german translation (#727) 2023-10-21 16:48:05 +02:00
dyphire
066a6dcf4e fix: update simplified chinese translation (#725) 2023-10-21 16:47:46 +02:00
tomasklaen
5e757b2c34 fix: font scaling in timeline
closes #722
2023-10-21 14:28:53 +02:00
tomasklaen
7663485d46 5.0.0 2023-10-21 12:52:09 +02:00
tomasklaen
a3fd4a28e2 tweak: removed paused from default timeline_persistency
Dunno why it was the default. It's annoying :)
2023-10-21 11:32:09 +02:00
Tomas Klaen
a0dccc75ca feat: improved escape and backspace behavior in menus (#719)
`escape` now only closes the menu, and nothing else (before it would clear search if present first).

New shortcuts:

- `ctrl+backspace` - Delete search query by word.
- `shift+backspace` - Clear search query.
2023-10-21 09:26:35 +02:00
tomasklaen
4fe9a73c57 refactor: various code reshuffling and renames
- Moved menu opening functions to `menus.lua`
- Renamed `make_` function prefixes to `create_` (I prefer `make_`, but `mp.*` APIs use `create_` so lets be consistent).
- Removed some dead code.
2023-10-20 10:23:21 +02:00
tomasklaen
230f625556 feat: renamed inputs command to keybinds
`inputs` could have a lot of alternate meanings. `keybinds` is much more clear what it represents.

ref #716
2023-10-20 09:57:58 +02:00
christoph-heinrich
769777ad8a feat(api): removed get-version in favor of early uosc-version broadcast (#714)
Detecting uosc via `get-version` reply sometimes took too long for "do something on init" sensitive tasks. This commit removes it and instead broadcasts `uosc-version <version>` message globally as a first thing during initialization phase.
2023-10-19 21:37:17 +02:00
tomasklaen
c534144adb tweak: made tooltips & thumbnail in timeline use same gaps both horizontally and vertically 2023-10-19 10:05:08 +02:00
christoph-heinrich
bd6a3d4ee5 fix: update german translation (#701) 2023-10-19 09:50:56 +02:00
Tomas Klaen
8763d3fdec feat: added update command to update uosc (#700)
`update` command spawns a subprocess using `mp.command_native_async()` that fetches an appropriate installer from uosc's main branch and runs it in powershell or bash.

This is pretty fragile and doesn't work everywhere, such as snap packages on Linux, or mpv on MacOS.

In snap on Linux, I think we are not allowed to use commands like `curl` or `unzip`, even if they're installed. I guess snap's sandboxing just hides them from us, even if we use a direct path to the binary on the system.

On MacOS, we get a weird `(23) Failed writing body` error... no idea.
2023-10-19 09:50:24 +02:00
tomasklaen
af04ab1e74 fix: missing delete-file-prev command
ref #715
2023-10-18 22:23:34 +02:00
tomasklaen
5ed36d3aec build: fix config path used by MacOS installer 2023-10-18 19:11:07 +02:00
tomasklaen
d5709a76ee fix: default colors not using correct format 2023-10-18 00:17:51 +02:00
christoph-heinrich
3943237d00 fix: touch events firing before handlers are bound (#708) 2023-10-17 22:33:16 +02:00
tomasklaen
03ff5cf9ec build: improved install scripts
The install directory can now be force with `MPV_CONFIG_DIR` environment variable.

Linux installer now detects common Flatpak and Snap packages and installs into their config locations.

All installers now backup current files and restore them when anything goes wrong.
2023-10-17 22:12:02 +02:00
tomasklaen
48ee23eeec fix: use the preferred portable shebang in unix installer
ref #700
2023-10-16 13:49:50 +02:00
christoph-heinrich
bc87eeba22 fix: render after menu fling (#707) 2023-10-16 10:06:59 +02:00
christoph-heinrich
377a262467 fix: position update before mbtn_left down handler (#705) 2023-10-16 10:06:31 +02:00
christoph-heinrich
def505c525 tweak: transparent outline for full volume icon (#704) 2023-10-16 10:06:06 +02:00
christoph-heinrich
99a63baa6b feat: configurable background opacity for buffering indicator (#703) 2023-10-15 17:25:56 +02:00
tomasklaen
cffef038f4 refactor: tween nan prevention 2023-10-15 17:18:47 +02:00
tomasklaen
62a77c5cdc fix: division by zero when animating from and to same value
closes #702
2023-10-15 17:06:56 +02:00
tomasklaen
910074af7b chore: remove typo from cSpell.words 2023-10-15 17:00:37 +02:00
tomasklaen
6075e67b10 fix: typo in cursor event binding 2023-10-15 16:31:21 +02:00
tomasklaen
de877b4cc9 feat: right click on volume or speed to reset them
Speed could already do it with left click, but it's better to move it to a consistent key to match volume, which uses left click to set the value.
2023-10-15 16:00:36 +02:00
tomasklaen
e9453fb579 refactor: cursor event handling
Split cursor into its own `lib/` file and added persistent event handling via `:on()`, `:off()`, and `:once()` methods.

Cursor now also listens on secondary pointer events.
2023-10-15 15:51:45 +02:00
tomasklaen
6ef76cb3ad feat!: reworked color options
Color options have been removed:

```
foreground
foreground_text
background
background_text
```

and replaced with a single `color` option containing comma delimited color overrides with more colors to edit.

Example:

```config
color=foreground=ffccaa,curtain=225566
```
2023-10-15 13:22:48 +02:00
tomasklaen
2ddc4d11f0 tweak: bumped curtain brightness and opacity to prevent menus blending into background 2023-10-15 11:17:07 +02:00
tomasklaen
e068752d00 tweak: prevent dynamic volume icon from shifting horizontally
ref #699
2023-10-15 11:06:35 +02:00
dyphire
06cef7c730 feat: dynamic volume icon (#699) 2023-10-15 10:59:06 +02:00
tomasklaen
3e685c52b8 style: reconfigured and run the whole codebase through lua formatter 2023-10-14 17:02:51 +02:00
dyphire
3679ba3278 fix: update simplified chinese translation (#698) 2023-10-14 13:24:58 +02:00
tomasklaen
c2aecac245 chore: changed license to LGPL-2.1
closes #682, closes #586
2023-10-14 12:23:16 +02:00
tomasklaen
6dfef3d3f8 feat!: changed modifier to force open directory in file menus from ctrl to alt
To force open a directory instead of navigating to it, you now need to use `alt+enter` or `alt+click` (previously `ctrl`).

`ctrl+enter` has been reassigned to search submit.
2023-10-14 12:07:25 +02:00
tomasklaen
074a1ee4ee docs: update readme with menu search info and other tweaks 2023-10-14 11:54:02 +02:00
tomasklaen
a888bf3466 fix: no window border on older windows mpv versions 2023-10-14 11:41:08 +02:00
tomasklaen
d42c152135 fix: hide audio indicator when displaying static pause indicator 2023-10-14 11:24:39 +02:00
tomasklaen
b6736815ac refactor: use comma_split where appropriate 2023-10-14 11:20:31 +02:00
tomasklaen
aeaf562b44 feat: added idle indicator, and an ability to control indicator opacities 2023-10-14 11:10:39 +02:00
Tomas Klaen
3af5ccff9f feat: added disable_elements option and disable-elements script message (#695)
* feat: added `disable_elements` option and `disable-elements` script message

Allows disabling elements and various indicators by adding their IDs to the list:

```conf
disable_elements=timeline,audio_indicator
```

Also includes a new script message listener `disable-elements`, that does the same thing:

```lua
local id = mp.get_script_name()
mp.commandv('script-message-to', 'uosc', 'disable-elements', id, 'timeline,audio_indicator')
```

It'll register what elements each script wants disabled. The element will be enabled only when it is not disabled by neither user nor any script.

To cancel or re-enable the elements, just pass an empty list:

```lua
mp.commandv('script-message-to', 'uosc', 'disable-elements', id, '')
```

ref #686, closes #592
2023-10-14 10:09:47 +02:00
Tomas Klaen
b7529ea59a feat: scripts to install or update uosc with a single command (#691) 2023-10-12 18:00:06 +02:00
christoph-heinrich
7467e182d3 refactor: use math.huge (#692) 2023-10-11 20:47:38 +02:00
tomasklaen
3c5d814307 fix: element fadeout bugs when cursor leaves/re-enters window
- Controls weren't fading out.
- Some elements needed to fully fade out until they started reacting to proximity on re-enter.

closes #688
2023-10-11 18:03:52 +02:00
tomasklaen
a883ae9b41 feat: changed animation_factor option to animation_duration
Having the option be in milliseconds is more intuitive.

closes #689
2023-10-11 17:51:42 +02:00
tomasklaen
6138c2075d revert: home/end in menus are animated again 2023-10-11 09:28:41 +02:00
christoph-heinrich
4949930e01 fix: updated german translation (#684) 2023-10-09 13:41:18 +02:00
Tomas Klaen
6a91ad9339 feat: support for creating menu titles and separators in input.conf (#681)
* feat: support for creating menu titles and separators in `input.conf`

When defining menu items in `input.conf`, it is now possible to add an un-selectable+muted+italic menu items by using `#` as key, and omitting the command:

```
#    #! Section > Title
```

You can also add a separator between previous and next item by using `---` as title:

```
#    #! Section > ---
```

closes #69
2023-10-09 11:53:24 +02:00
tomasklaen
27fa983046 feat: added audio indicator for audio files without cover
ref #686
2023-10-09 11:41:07 +02:00
tomasklaen
c5de0823fe fix: elements instantly disappearing instead of fading out on mouse leave
ref #685
2023-10-09 11:09:50 +02:00
tomasklaen
484c553686 refactor: mouse position initialization
ref #687
2023-10-09 10:44:37 +02:00
tomasklaen
61ce3291f7 feat: navigating menu with up/down/home/end keys is now instant with no animation
Left Page Up/Down as the animation is useful there (gives a sense of where the menu has animated from & to), but up/down/home/end just work better when instant.
2023-10-08 14:28:46 +02:00
tomasklaen
1440fde298 feat: added animation_factor option to control animation speed
closes #471
2023-10-08 14:21:55 +02:00
Tomas Klaen
b800aebeff feat!: reworked opacity options (#680)
These options have been removed:

```
timeline_opacity
timeline_chapters_opacity
volume_opacity
speed_opacity
menu_opacity
menu_parent_opacity
top_bar_title_opacity
window_border_opacity
curtain_opacity
```

and replaced with one comma delimited `opacity` list to override all of the above and some new default opacity values.

Example:

```
opacity=timeline=0.5,title=0.5
```

List of opacity values available for override and their current defaults:

```
timeline=.9
position=1     # timeline progress or line
chapters=0.8
slider=0.9     # background of all sliders, such as volume
slider_gauge=1 # value bar of all sliders
speed=0.6
menu=1
submenu=0.4
border=1       # window border
title=1        # window title
tooltip=1      # tooltip background
thumbnail=1    # thumbnail border
curtain=0.5
```

closes #584, closes #636, closes #675
2023-10-08 11:59:56 +02:00
christoph-heinrich
2f46e23a9f feat: improved menu title & hint clipping logic (#668) 2023-10-07 16:24:03 +02:00
tomasklaen
f6837cab74 fix: default items not cached when input.conf was empty
closes #674
2023-10-07 11:32:41 +02:00
christoph-heinrich
149b009477 fix: user-data/uosc/menu/type backwards compatibility, and being nil means closed (#673)
`nil` being a valid menu type in the provided property was a mistake.
For backwards compatiblity we also set shared-script-properties now.
2023-10-07 11:29:17 +02:00
christoph-heinrich
51d45474e2 tweak: remove text shadow in menu (#679) 2023-10-07 11:23:19 +02:00
christoph-heinrich
f25dfc889e fix: volume slider crash when no audio (#678)
d2febcbd47 forgot the second parameter,
resulting in a crash because it's nil.
2023-10-07 10:46:04 +02:00
christoph-heinrich
73dc2ccf00 fix: updating the menu while using the internal search (#677)
Fixes #671
2023-10-07 10:45:07 +02:00
tomasklaen
eacfd6391d fix: search_submenus inheritance, missing docs, and ass_safe_title cache not clearing
ref #677
2023-10-07 00:16:19 +02:00
christoph-heinrich
a94db0c799 fix: window border enable logic for non windows (#676)
`top-bar` is only relevant on windows
2023-10-06 23:31:48 +02:00
tomasklaen
a4ac44a7dc feat: added border_radius option
closes #377
2023-10-06 23:24:20 +02:00
Tomas Klaen
d2febcbd47 feat!: reworked config options for fullscreen scale adjustments (#664)
* feat!: reworked config options for fullscreen scale adjustments

Removed options:

```
timeline_size_fullscreen
controls_size_fullscreen
volume_size_fullscreen
menu_item_height_fullscreen
menu_min_width_fullscreen
top_bar_size_fullscreen
```

Additionally, `ui_scale` has been renamed to `scale`.

The scaling can now be controlled by these two new options:

```
scale=1
scale_fullscreen=1.3
```

closes #543
2023-10-06 23:20:14 +02:00
Tomas Klaen
473278c4bd feat: inputs command to display a palette menu with all active keybindings (#665) 2023-10-06 11:12:38 +02:00
tomasklaen
e7371f61e0 fix: progress line width broken in #661
ref #661
2023-10-04 09:36:22 +02:00
tomasklaen
c324e07ff0 feat: windowed and fullscreen added to available element persistency states
closes #618
2023-10-03 13:32:40 +02:00
Tomas Klaen
7f9a8cae6d feat: reworked timeline progress mode (#661)
* feat: reworked timeline progress mode

Config options:

```
timeline_line_width_fullscreen=3
timeline_line_width_minimized_scale=10
timeline_size_min=2
timeline_size_max=40
timeline_size_min_fullscreen=0
timeline_size_max_fullscreen=60
timeline_start_hidden=no
```

have been replaced with:

```
timeline_size=40
timeline_size_fullscreen=60
progress=windowed
progress_size=2
progress_line_width=20
```

This simplifies stuff a bunch, and enables timeline's progress mode to be togglable at all times. Previously you could only toggle when it was configured to be visible in current context.

closes #630
2023-10-03 11:10:50 +02:00
dyphire
a0c544a4ba fix: add sbv to subtitle types (#663) 2023-10-02 17:54:09 +02:00
christoph-heinrich
2cad0d414e fix: menu title and search not clipped properly in extremely tiny windows (#658) 2023-10-02 17:02:22 +02:00
xfzv
5bf81228db fix: add cue to audio types (#662) 2023-10-02 15:44:11 +02:00
dyphire
3ac22679f5 fix: update simplified chinese translation (#660) 2023-10-02 10:40:42 +02:00
christoph-heinrich
4f519f45e1 feat: update german translation (#659) 2023-10-02 10:39:48 +02:00
Tomas Klaen
aab6d2ef13 fix: menu positioning and sizing polish (#654) 2023-10-02 10:38:37 +02:00
christoph-heinrich
483acc22d9 fix: search backspace deleting unicode characters (#657) 2023-10-01 20:32:27 +02:00
Tomas Klaen
cbfb2a62fb feat(api): added search_submenus prop to menus (#655)
`search_submenus` makes uosc's internal search handler (when no `on_search` callback is defined) look into submenus as well, effectively flattening the menu for the duration of the search.
2023-09-30 17:24:03 +02:00
christoph-heinrich
512281ac4b feat(api)!: menu script-message changes (#653)
- `open-menu` now only opens the menu, while closing any existing one first, even if it has the same `type`.
- `update-menu` will only update a currently opened menu of the same type. If no menu is open, or current menu's type is different, it doesn't do anything.
- `close-menu [type]` can be used to close any currently opened menu when called without a `[type]` argument, or only a menu of `[type]` type.
- uosc now keeps track of a currently opened menu type on the `user-data/uosc/menu/type` property, accessible via `mp.get_property_native('user-data/uosc/menu/type')`. This property is `nil` if no uosc menu is opened.

This is to achieve a predictable and granular control of menus with no implicit magic going on in the background.

The main difference is that `open-menu` can no longer be used to toggle the menu. You have to implement toggling manually by calling `open-menu` or `close-menu [type]` when appropriate. You can check if your menu is still opened either by getting or observing the `user-data/uosc/menu/type` property, or using the `on_close` menu callback.
2023-09-30 17:11:37 +02:00
tomasklaen
c88c476ec7 fix: search_suggestion submitting externally handled searches
ref #652
2023-09-30 16:57:36 +02:00
Tomas Klaen
9b251efbd9 feat(api): palette menus (#652)
Each (sub)menu can now enable palette mode by setting its `palette` property to `true`.

In this mode:

- search input is always visible, doesn't have to be enabled, and can't be disabled
- `title` is used as input placeholder while search is empty
- `search_suggestion` can be used to pre-populate search input with an initial query
2023-09-29 23:08:59 +02:00
Tomas Klaen
4b6cc2b8a7 feat: redesigned menu title & search input (#650)
- simpler & less cluttered design without textures
- search icon
- clickable icon for submittable searches
- search cursor
- localized placeholders
- clicking empty title now does nothing instead of closing the menu
2023-09-27 16:14:04 +02:00
Tomas Klaen
d9535b4151 fix: search not selecting 1st item in results (#651) 2023-09-27 15:51:43 +02:00
christoph-heinrich
00edb9659a refactor: rename first_word_chars() to initials() (#649)
I didn't like the name from the very beginning, but I couldn't think of
something better. Ever since I realized that such characters are called
"initials", this name bothered me every time I saw it.
2023-09-26 22:12:00 +02:00
christoph-heinrich
c6d89b2537 fix: render spaces at the end of the search query (#648)
Currently the text does not advance to the left when spaces are entered,
which must mean that libass trims any trailing spaces.
Appending a ZWNBSP at the end prevents any trimming.
2023-09-24 20:23:21 +02:00
tomasklaen
3766f583ae feat: added show_hidden_files option
This is currently limited to Mac & Linux only, since on Windows we have no way of telling a file is hidden (`file_info()` doesn't expose this information).

closes #647
2023-09-24 15:14:58 +02:00
tomasklaen
fec33b73f3 feat: added top_bar_flash_on option
closes #639
2023-09-24 14:41:21 +02:00
christoph-heinrich
c277acafd3 feat: search by first character of each word (#644) 2023-09-23 23:21:29 +02:00
christoph-heinrich
18170e2405 refactor: use itable_has for text wrapping (#646)
* refactor: use itable_has for text wrapping

* refactor: declare static lists outside of function
2023-09-23 23:19:42 +02:00
dyphire
442967b73b fix: adapt to the new title-bar state of mpv (#643)
mpv has added a new option to control window title bar state,
which requires relevant adjustments to be added in uosc
2023-09-23 23:17:17 +02:00
christoph-heinrich
9628d32937 fix: stream quality selection (#641)
Due to a recent change in mpv writing to playlist-pos doesn't restart
playback anymore, which was necessary for the format change to work for
the current file.
4b2276b730
2023-09-22 14:28:08 +02:00
christoph-heinrich
b1d39afe19 fix: external search in menu (#642) 2023-09-22 11:42:36 +02:00
christoph-heinrich
22d45b6c3b fix: prepare for shared-script-properties removal (#640)
mpv is about to remove `shared-script-properties`, which will cause
`utils.shared_script_property_set` to be nil.
Check for it's existence before calling it.
2023-09-22 11:41:25 +02:00
christoph-heinrich
f067ea336e fix: case insensitive menu search (#638)
The title was lowered, but not the query.
As a result searching for uppercase letters would never match anything.

Fixes #637
2023-09-21 20:51:17 +02:00
christoph-heinrich
18adf90af6 feat: searchable menus (#625)
* feat: make the menu searchable

Each menu/submenu can have a `on_search` and `search_debounce`.
`search_debounce` supports the special value 'submit' with which
searches only get executed upon pressing ctrl+enter.

Without a `on_search` an internal search implementation gets used.

The internal search has it's own menu update function that is needed to
keep the same menu item objects around because otherwise updates to
children get discarded when the parent search gets updated.

* feat: add `menu_type_to_search` option

Always listening to text breaks toggling the menu via a single keybind.
Add an option so users can choose to manually activate the search.
2023-09-21 10:28:43 +02:00
Tomas Klaen
7a4d687864 fix: shuffle history couldn't go back more than once (#635)
* fix: shuffle history couldn't go back more than once

ref #631

---------

Co-authored-by: christoph-heinrich <christoph-heinrich@users.noreply.github.com>
2023-09-21 10:27:21 +02:00
christoph-heinrich
230112bde8 fix: selected_index norm in reset_navigation() (#634)
It attempts to do what select_by_offset already does but worse and
the check also couldn't have been right.

Additionally select_by_offset() was missing a request_render(), but that
seems to never actually make a difference.
2023-09-20 12:05:47 +02:00
tomasklaen
5235272ceb fix: prev commands now play previously played file even when shuffle is enabled
closes #631
2023-09-19 23:54:52 +02:00
christoph-heinrich
bff4da80d3 fix: crash when using wrong syntax for speed scale (#633)
A speed control diffinition like e.g. `speed:{1.8}` would result in
a crash because the `scale` gets set to `{1.8}` which isn't a string
lua can convert to a number on the fly.

Fixes #632
2023-09-19 22:03:37 +02:00
christoph-heinrich
b4037ec471 fix: menu height and positioning (#629)
It was possible for menus with titles to clip on the bottom edge.
They also weren't sized and positioned the same as menus without titles.
2023-09-18 11:18:44 +02:00
christoph-heinrich
8fe748c4d2 feat: initial support for updating options at runtime (#571)
Options can be changed during runtime by changing `script-opts`.
So far such option changes were simply ignored.
Now most options work, and the rest can be implemented when the needed.
2023-09-18 11:13:48 +02:00
christoph-heinrich
f62a9d1bbb fix: don't select not selectable items (#627)
It was possible for not selectable items to be selected via page up/down
as well as when updating the menu.

Page down now goes to the next best selectable item after adding to the
index, like is the case for next(), with a fallback to an earlier index.
Page up is the same as page down but inverted direction.

Updating the menu now chooses the closes selectable item to the intended
index.

* refactor: unify menu item navigation
2023-09-17 11:55:21 +02:00
christoph-heinrich
d570e05388 fix: top bar enabled checks (#628)
`top_bar_title` was checked like a bool, but it is a string, so it was
always treated as true.
Playlist checks were also missing.
2023-09-17 11:53:54 +02:00
christoph-heinrich
ab7bee5e44 fix: crash when selecting "Empty" in chapters menu (#626)
The menu item representing an empty menu is supposed to be not
selectable, however it was possible to select it with the mouse.
That results in a crash in the chapters menu because it subtracts one
from the value, which is "ignore" for the "Empty" item.
2023-09-16 12:43:04 +02:00
tomasklaen
e897089f61 fix: vertically misaligned number in playlist indicator
closes #623
2023-09-12 22:24:00 +02:00
christoph-heinrich
f03b04496b feat: added background to tooltips (#621)
The text can sometimes be hard to read depending on the image behind it.
Drawing a background helps with readability in such situations.
2023-09-12 21:22:22 +02:00
tomasklaen
350f54f321 feat: open chapters menu when clicking on current chapter in title 2023-09-12 12:20:56 +02:00
Christoph Heinrich
d25f3e2e99 refactor: use new velocity measurement for menu flings 2023-08-25 09:13:45 +02:00
Christoph Heinrich
3f21df1a9d refactor: use cicular buffer for cursor.history
Circular buffers are more efficient then moving the entries around.

Cursor velocity is now calculated on the fly, with from the current
cursor position and time as well a cursor position that is either
the youngest sample >100ms old, or the oldest sample in the buffer,
which is 10 elements in size. Restricting the sample selection based on
time prevents the filter window from becomming too long in case of
setups with low cursor update frequency, while still bring more
responsive velocity measurements to systems with higher cursor update
frequencies.

Removed the timer for exact seeking after fast seeks, as fast/exact
seeks can now be controlled with cursor speed and thus there is no need
for this anymore.
2023-08-25 09:13:45 +02:00
tomasklaen
776ca17f66 feat: fast seek in timeline based on cursor velocity 2023-08-25 09:13:45 +02:00
Tomas Klaen
d1e9f9c4eb fix: stale (closed) menus triggering input events (#612) 2023-08-23 18:13:43 +02:00
christoph-heinrich
6606f3e11f fix: remove redundant symbol order from sorting algorithm (#608)
We were already using the same sorting algorithm as autoload.lua,
but with the addition of a custom symbol order. However that symbol
order was only applied to the first character, so when sorting absolute
paths it doesn't actually make a difference.

Using the exact same algorithm for autoload.lua and uosc also makes it
easier to apply any future improvements from one to the other.
2023-08-21 18:05:01 +02:00
tomasklaen
68b5b2aaed refactor: improved protocol matching patterns
ref #605
2023-08-16 12:52:07 +02:00
tomasklaen
f857d468e1 fix: error in matching pattern caused some files to be recognized as magnet links
closes #605
2023-08-16 10:07:36 +02:00
Andrei Shevchuk
bc7b1a12bc feat: add russian translation (#597) 2023-08-02 09:13:07 +02:00
/ˈɛvən/
cad0d174d7 fix: make the install procedure on linux use XDG_CONFIG_HOME (#595) 2023-08-01 09:51:40 +02:00
Felix Yan
71cdeda7b3 chore: correct typos in uosc.conf (#594) 2023-08-01 09:47:27 +02:00
75 changed files with 8681 additions and 4110 deletions

View File

@@ -1,83 +1,167 @@
[*.lua]
# see https://github.com/CppCXY/EmmyLuaCodeStyle
[*]
indent_style = tab
indent_size = 4
end_of_line = lf
insert_final_newline = true
[*.md]
indent_style = space
indent_size = 2
# see https://github.com/CppCXY/EmmyLuaCodeStyle
[*.lua]
# [basic]
# optional space/tab
indent_style = tab
# if indent_style is space, this is valid
indent_size = 4
# if indent_style is tab, this is valid
tab_width = 4
# none/single/double
quote_style = single
continuation_indent_size = 4
continuation_indent = 4
# this mean utf8 length , if this is 'unset' then the line width is no longer checked
# this option decides when to chopdown the code
max_line_length = 120
# crlf/lf/cr/auto
# optional crlf/lf/cr/auto, if it is 'auto', in windows it is crlf other platforms are lf
# in neovim the value 'auto' is not a valid option, please use 'unset'
end_of_line = lf
detect_end_of_line = false
insert_final_newline = true
# [function]
# none/ comma / semicolon / only_kv_colon
table_separator_style = comma
# true/false/only_after_more_indention_statement/only_not_exist_cross_row_expression
align_call_args = false
align_function_define_params = true
remove_expression_list_finish_comma = true
# keep/remove/remove_table_only/remove_string_only/unambiguous_remove_string_only
#optional keep/never/always/smart
trailing_table_separator = smart
# keep/remove/remove_table_only/remove_string_only
call_arg_parentheses = keep
# [table]
detect_end_of_line = false
# none/comma/semicolon
table_separator_style = comma
# keep/never/always/smart
trailing_table_separator = smart
# align equal signs in tables
continuous_assign_table_field_align_to_equal_sign = false
# if true "local t = { 1, 2, 3 }"
keep_one_space_between_table_and_bracket = false
# [statement]
align_chained_expression_statement = false
max_continuous_line_distance = 1
# align equal signs in value assignments
continuous_assign_statement_align_to_equal_sign = false
if_condition_align_with_each_other = false
local_assign_continuation_align_to_first_expression = false
statement_inline_comment_space = 1
# [indentation]
# labels (e.g.::continue::) will not be intended
label_no_indent = false
# no indentation for do statement
do_statement_no_indent = false
# no indentation for conditions of an if statement when on new line
if_condition_no_continuation_indent = false
if_branch_comments_after_block_no_indent = false
# this will check text end with new line
insert_final_newline = true
# [space]
space_around_table_field_list = false
space_before_attribute = false
# if true, t[#t+1] will not space wrapper '+'
table_append_expression_no_space = false
long_chain_expression_allow_one_space_after_colon = false
remove_empty_header_and_footer_lines_in_function = true
space_before_function_open_parenthesis = false
space_before_function_call_open_parenthesis = false
space_before_closure_open_parenthesis = false
# optional always/only_string/only_table/none
# or true/false
space_before_function_call_single_arg = always
space_before_open_square_bracket = false
# format like this "local t <const> = 1"
keep_one_space_between_namedef_and_attribute = false
# [row_layout]
# Each can be: minLine:${n}, keepLine, keepLine:${n}, maxLine:${n}
space_inside_function_call_parentheses = false
keep_line_after_if_statement = keepLine
keep_line_after_do_statement = keepLine
keep_line_after_while_statement = keepLine
keep_line_after_repeat_statement = keepLine
keep_line_after_for_statement = keepLine
keep_line_after_local_or_assign_statement = keepLine
keep_line_after_function_define_statement = keepLine
keep_line_after_expression_statement = keepLine
space_inside_function_param_list_parentheses = false
# [diagnostic]
# creates a lot of warnings I can't do anything about so disable it is
space_inside_square_brackets = false
enable_check_codestyle = false
# like t[#t+1] = 1
space_around_table_append_operator = false
ignore_spaces_inside_function_call = false
space_before_inline_comment = 1
# [operator space]
space_around_math_operator = true
space_after_comma = true
space_after_comma_in_for_statement = true
# true/false or none/always/no_space_asym
space_around_concat_operator = true
space_around_logical_operator = true
# true/false or none/always/no_space_asym
space_around_assign_operator = true
# [align]
align_call_args = false
align_function_params = false
align_continuous_assign_statement = false
align_continuous_rect_table_field = false
align_continuous_line_space = 2
align_if_branch = false
# option none / always / contain_curly/
align_array_table = none
align_continuous_similar_call_args = false
align_continuous_inline_comment = false
# option none / always / only_call_stmt
align_chain_expr = none
# [indent]
never_indent_before_if_condition = false
never_indent_comment_on_if_branch = true
keep_indents_on_empty_lines = false
# [line space]
# The following configuration supports four expressions
# keep
# fixed(n)
# min(n)
# max(n)
# for eg. min(2)
line_space_after_if_statement = keep
line_space_after_do_statement = keep
line_space_after_while_statement = keep
line_space_after_repeat_statement = keep
line_space_after_for_statement = keep
line_space_after_local_or_assign_statement = keep
line_space_after_function_statement = keep
line_space_after_expression_statement = keep
line_space_after_comment = keep
line_space_around_block = fixed(1)
# [line break]
break_all_list_when_line_exceed = false
auto_collapse_lines = false
break_before_braces = false
# [preference]
ignore_space_after_colon = false
remove_call_expression_list_finish_comma = false
# keep / always / same_line / repalce_with_newline
end_statement_with_semicolon = keep

2
.gitignore vendored
View File

@@ -1 +1,3 @@
src/uosc/bin
release
*.zip

View File

@@ -8,7 +8,8 @@
],
"Lua.diagnostics.disable": [
"lowercase-global",
"redefined-local"
"redefined-local",
"duplicate-set-field"
],
"Lua.diagnostics.globals": [
"mp"

674
LICENSE
View File

@@ -1,674 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

502
LICENSE.LGPL Normal file
View File

@@ -0,0 +1,502 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
This license, the Lesser General Public License, applies to some
specially designated software packages--typically libraries--of the
Free Software Foundation and other authors who decide to use it. You
can use it too, but we suggest you first think carefully about whether
this license or the ordinary General Public License is the better
strategy to use in any particular case, based on the explanations below.
When we speak of free software, we are referring to freedom of use,
not price. Our General Public Licenses are designed to make sure that
you have the freedom to distribute copies of free software (and charge
for this service if you wish); that you receive source code or can get
it if you want it; that you can change the software and use pieces of
it in new free programs; and that you are informed that you can do
these things.
To protect your rights, we need to make restrictions that forbid
distributors to deny you these rights or to ask you to surrender these
rights. These restrictions translate to certain responsibilities for
you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link other code with the library, you must provide
complete object files to the recipients, so that they can relink them
with the library after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
We protect your rights with a two-step method: (1) we copyright the
library, and (2) we offer you this license, which gives you legal
permission to copy, distribute and/or modify the library.
To protect each distributor, we want to make it very clear that
there is no warranty for the free library. Also, if the library is
modified by someone else and passed on, the recipients should know
that what they have is not the original version, so that the original
author's reputation will not be affected by problems that might be
introduced by others.
Finally, software patents pose a constant threat to the existence of
any free program. We wish to make sure that a company cannot
effectively restrict the users of a free program by obtaining a
restrictive license from a patent holder. Therefore, we insist that
any patent license obtained for a version of the library must be
consistent with the full freedom of use specified in this license.
Most GNU software, including some libraries, is covered by the
ordinary GNU General Public License. This license, the GNU Lesser
General Public License, applies to certain designated libraries, and
is quite different from the ordinary General Public License. We use
this license for certain libraries in order to permit linking those
libraries into non-free programs.
When a program is linked with a library, whether statically or using
a shared library, the combination of the two is legally speaking a
combined work, a derivative of the original library. The ordinary
General Public License therefore permits such linking only if the
entire combination fits its criteria of freedom. The Lesser General
Public License permits more lax criteria for linking other code with
the library.
We call this license the "Lesser" General Public License because it
does Less to protect the user's freedom than the ordinary General
Public License. It also provides other free software developers Less
of an advantage over competing non-free programs. These disadvantages
are the reason we use the ordinary General Public License for many
libraries. However, the Lesser license provides advantages in certain
special circumstances.
For example, on rare occasions, there may be a special need to
encourage the widest possible use of a certain library, so that it becomes
a de-facto standard. To achieve this, non-free programs must be
allowed to use the library. A more frequent case is that a free
library does the same job as widely used non-free libraries. In this
case, there is little to gain by limiting the free library to free
software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free
programs enables a greater number of people to use a large body of
free software. For example, permission to use the GNU C Library in
non-free programs enables many more people to use the whole GNU
operating system, as well as its variant, the GNU/Linux operating
system.
Although the Lesser General Public License is Less protective of the
users' freedom, it does ensure that the user of a program that is
linked with the Library has the freedom and the wherewithal to run
that program using a modified version of the Library.
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, whereas the latter must
be combined with the library in order to run.
GNU LESSER GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other
program which contains a notice placed by the copyright holder or
other authorized party saying it may be distributed under the terms of
this Lesser General Public License (also called "this License").
Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work
which has been distributed under these terms. A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for
making modifications to it. For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control compilation
and installation of the library.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
(For example, a function in a library to compute square roots has
a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Library.
In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable "work that
uses the Library", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (1) uses at run time a
copy of the library already present on the user's computer system,
rather than copying library functions into the executable, and (2)
will operate properly with a modified version of the library, if
the user installs one, as long as the modified version is
interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
d) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
e) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the materials to be distributed need not include anything that is
normally distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties with
this License.
11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
13. The Free Software Foundation may publish revised and/or new
versions of the Lesser General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest
possible use to the public, we recommend making it free software that
everyone can redistribute and change. You can do so by permitting
redistribution under these terms (or, alternatively, under the terms of the
ordinary General Public License).
To apply these terms, attach the following notices to the library. It is
safest to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
<one line to give the library's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the library, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
<signature of Ty Coon>, 1 April 1990
Ty Coon, President of Vice
That's all there is to it!

399
README.md
View File

@@ -4,82 +4,96 @@
Feature-rich minimalist proximity-based UI for <a href="https://mpv.io">MPV player</a>.
</p>
<br/>
<a href="https://user-images.githubusercontent.com/47283320/195073006-bfa72bcc-89d2-4dc7-b8dc-f3c13273910c.webm"><img src="https://user-images.githubusercontent.com/47283320/195072935-44d591d9-00bb-4a55-8795-9cf81f65d397.png" alt="Preview screenshot"></a>
<a href="https://user-images.githubusercontent.com/47283320/195073006-bfa72bcc-89d2-4dc7-b8dc-f3c13273910c.webm"><img src="https://github.com/tomasklaen/uosc/assets/47283320/9f99f2ae-3b65-4935-8af3-8b80c605f022" alt="Preview screenshot"></a>
</div>
Most notable features:
Features:
- UI elements hide and show based on their proximity to cursor instead of every time mouse moves. This gives you 100% control over when you see the UI and when you don't. Click on the preview above to see it in action.
- Set min timeline size to make an always visible discrete progress bar.
- UI elements hide and show based on their proximity to cursor instead of every time mouse moves. This provides 100% control over when you see the UI and when you don't. Click on the preview above to see it in action.
- When timeline is unused, it can minimize itself into a small discrete progress bar.
- Build your own context menu with nesting support by editing your `input.conf` file.
- Configurable controls bar.
- Fast and efficient thumbnails with [thumbfast](https://github.com/po5/thumbfast) integration.
- UIs for:
- Loading external subtitles.
- Selecting subtitle/audio/video track.
- [Downloading subtitles](#download-subtitles) from [Open Subtitles](https://www.opensubtitles.com).
- Loading external subtitles.
- Selecting stream quality.
- Quick directory and playlist navigation.
- All menus are instantly searchable. Just start typing.
- Mouse scroll wheel does multiple things depending on what is the cursor hovering over:
- Timeline: seek by `timeline_step` seconds per scroll.
- Volume bar: change volume by `volume_step` per scroll.
- Speed bar: change speed by `speed_step` per scroll.
- Just hovering video with no UI widget below cursor: your configured wheel bindings from `input.conf`.
- Transform chapters into timeline ranges (the red portion of the timeline in the preview).
- And a lot of useful options and commands to bind keys to.
- Right click on volume or speed elements to reset them.
- Transforming chapters into timeline ranges (the red portion of the timeline in the preview).
- A lot of useful options and commands to bind keys to.
- [API for 3rd party scripts](https://github.com/tomasklaen/uosc/wiki) to extend, or use uosc to render their menus.
[Changelog](https://github.com/tomasklaen/uosc/releases).
## Download
## Install
- [`uosc.zip`](https://github.com/tomasklaen/uosc/releases/latest/download/uosc.zip) - main archive with script and its requirements
- [`uosc.conf`](https://github.com/tomasklaen/uosc/releases/latest/download/uosc.conf) - configuration file with default values and documentation
1. These commands will install or update **uosc** and place a default `uosc.conf` file into `script-opts` if it doesn't exist already.
## Installation
### Windows
1. Extract `uosc.zip` into your mpv config directory.
_Optional, needed to run a remote script the first time if not enabled already:_
_List of all the possible places where it can be located is documented here: https://mpv.io/manual/master/#files_
```powershell
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
```
On Linux and macOS these terminal commands can be used to install or update uosc (if wget and unzip are installed):
Run:
```powershell
irm https://raw.githubusercontent.com/tomasklaen/uosc/HEAD/installers/windows.ps1 | iex
```
_**NOTE**: If this command is run in an mpv installation directory with `portable_config`, it'll install there instead of `AppData`._
_**NOTE2**: The downloaded archive might trigger false positives in some antiviruses. This is explained in [FAQ below](#why-is-the-release-reported-as-malicious-by-some-antiviruses)._
### Linux & macOS
_Requires **curl** and **unzip**._
```sh
mkdir -pv ~/.config/mpv/script-opts/
rm -rf ~/.config/mpv/scripts/uosc_shared
wget -P /tmp/ https://github.com/tomasklaen/uosc/releases/latest/download/uosc.zip
unzip -od ~/.config/mpv/ /tmp/uosc.zip
rm -fv /tmp/uosc.zip
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/tomasklaen/uosc/HEAD/installers/unix.sh)"
```
On Windows these equivalent PowerShell commands can be used:
```PowerShell
New-Item -ItemType Directory -Force -Path "$env:APPDATA/mpv/script-opts/"
$Folder = "$env:APPDATA/mpv/scripts/uosc_shared"
if (Test-Path $Folder) {
Remove-Item -LiteralPath $Folder -Force -Recurse
}
Invoke-WebRequest -OutFile "$env:APPDATA/mpv/uosc_tmp.zip" -Uri https://github.com/tomasklaen/uosc/releases/latest/download/uosc.zip
Expand-Archive "$env:APPDATA/mpv/uosc_tmp.zip" -DestinationPath "$env:APPDATA/mpv" -Force
Remove-Item "$env:APPDATA/mpv/uosc_tmp.zip"
On Linux, we try to detect what package manager variant of the config location you're using, with precedent being:
```
~/.var/app/io.mpv.Mpv (flatpak)
~/snap/mpv
~/snap/mpv-wayland
~/.config/mpv
```
2. **uosc** is a replacement for the built in osc, so that has to be disabled first.
To install into any of these locations, make sure the ones above it don't exist.
In your `mpv.conf` (file that should already exist in your mpv directory, if not, create it):
### Manual
1. Download & extract [`uosc.zip`](https://github.com/tomasklaen/uosc/releases/latest/download/uosc.zip) into your mpv config directory. (_See the [documentation of mpv config locations](https://mpv.io/manual/master/#files)._)
2. If you don't have it already, download & extract [`uosc.conf`](https://github.com/tomasklaen/uosc/releases/latest/download/uosc.conf) into `script-opts` inside your mpv config directory. It contains all of uosc options along with their default values and documentation.
2. **OPTIONAL**: `mpv.conf` tweaks to better integrate with **uosc**:
```config
# required so that the 2 UIs don't fight each other
osc=no
# uosc provides its own seeking/volume indicators, so you also don't need this
# uosc provides seeking & volume indicators (via flash-timeline and flash-volume commands)
# if you decide to use them, you don't need osd-bar
osd-bar=no
# uosc will draw its own window controls if you disable window border
# uosc will draw its own window controls and border if you disable window border
border=no
```
3. To configure **uosc**, create a `script-opts/uosc.conf` file, or download `uosc.conf` with all default values from the link above, and save into `script-opts/` folder.
3. **OPTIONAL**: To have thumbnails in timeline, install [thumbfast](https://github.com/po5/thumbfast). No other step necessary, **uosc** integrates with it seamlessly.
4. **OPTIONAL**: To have thumbnails in timeline, install [thumbfast](https://github.com/po5/thumbfast). That's it, no other step necessary, **uosc** integrates with it seamlessly.
5. **OPTIONAL**: If the UI feels sluggish/slow while playing video, you can remedy this a lot by placing this in your `mpv.conf`:
4. **OPTIONAL**: If the UI feels sluggish/slow while playing video, you can remedy this _a bit_ by placing this in your `mpv.conf`:
```config
video-sync=display-resample
@@ -89,11 +103,11 @@ Most notable features:
#### What is going on?
**uosc** places performance as one of its top priorities, so how can the UI feel slow? Well, it really isn't, **uosc** is **fast**, it just doesn't feel like it because when video is playing, the UI rendering frequency is chained to its frame rate, so unless you are the type of person that can't see above 24fps, it _will_ feel slow, unless you tell mpv to resample the video framerate to match your display. This is mpv limitation, and not much we can do about it on our side.
**uosc** places performance as one of its top priorities, but it might feel a bit sluggish because during a video playback, the UI rendering frequency is chained to its frame rate. To test this, you can pause the video which will switch refresh rate to be closer or match the frequency of your monitor, and the UI should feel smoother. This is mpv limitation, and not much we can do about it on our side.
## Options
All of the available **uosc** options with their default values and documentation are in the provided `uosc.conf` file.
All of the available **uosc** options with their default values are documented in [`uosc.conf`](https://github.com/tomasklaen/uosc/blob/HEAD/src/uosc.conf) file ([download](https://github.com/tomasklaen/uosc/releases/latest/download/uosc.conf)).
To change the font, **uosc** respects the mpv's `osd-font` configuration.
@@ -101,26 +115,30 @@ To change the font, **uosc** respects the mpv's `osd-font` configuration.
These bindings are active when any **uosc** menu is open (main menu, playlist, load/select subtitles,...):
- `up`, `down` - select previous/next item
- `left`, `right` - back to parent menu or close, activate item
- `enter` - activate item
- `esc` - close menu
- `wheel_up`, `wheel_down` - scroll menu
- `pgup`, `pgdwn`, `home`, `end` - self explanatory
- `ctrl+up/down` - move selected item in menus that support it (playlist)
- `del` - delete selected item in menus that support it (playlist)
- `shift+enter`, `shift+right` - activate item without closing the menu
- `ctrl+enter`, `ctrl+click` - force activate an item, even if it's a submenu. In practical terms: activates a directory instead of navigation to its contents.
- `up`, `down` - Select previous/next item.
- `enter` - Activate item or submenu.
- `bs` (backspace) - Activate parent menu.
- `esc` - Close menu.
- `wheel_up`, `wheel_down` - Scroll menu.
- `pgup`, `pgdwn`, `home`, `end` - Self explanatory.
- `ctrl+f` or `\` - In case `menu_type_to_search` config option is disabled, these two trigger the menu search instead.
- `ctrl+enter` - Submits a search in menus without instant search.
- `ctrl+backspace` - Delete search query by word.
- `shift+backspace` - Clear search query.
- `ctrl+up/down/pgup/pgdwn/home/end` - Move selected item in menus that support it (playlist).
- `del` - Delete selected item in menus that support it (playlist).
- `shift+enter`, `shift+click` - Activate item without closing the menu. Might not be supported by all menus.
- `alt+enter`, `alt+click` - In file navigating menus, opens a directory in mpv instead of navigating to its contents.
Click on a faded parent menu to go back to it.
## Commands
**uosc** provides various commands with useful features to bind your preferred keys to. These are all unbound by default.
**uosc** provides various commands with useful features to bind your preferred keys to, or populate your menu with. These are all unbound by default.
To add a keybind to one of this commands, open your `input.conf` file and add one on a new line. The command syntax is `script-binding uosc/{command-name}`.
Example to bind the `tab` key to peek timeline:
Example to bind the `tab` key to toggle the ui visibility:
```
tab script-binding uosc/toggle-ui
@@ -144,7 +162,7 @@ Under the hood, `toggle-ui` is using `toggle-elements`, and that is in turn usin
#### `toggle-progress`
Toggles the always visible portion of the timeline. You can look at it as switching `timeline_size_min` option between it's configured value and 0.
Toggles the timeline progress mode on/off. Progress mode is an always visible thin version of timeline with no text labels. It can be configured using the `progress*` config options.
#### `toggle-title`
@@ -156,7 +174,7 @@ Only relevant if top bar is enabled, `top_bar_alt_title` is configured, and `top
Command(s) to briefly flash the whole UI. Elements are revealed for a second and then fade away.
To flash individual elements, you can use: `flash-timeline`, `flash-top-bar`, `flash-volume`, `flash-speed`, `flash-pause-indicator`, `decide-pause-indicator`
To flash individual elements, you can use: `flash-timeline`, `flash-progress`, `flash-top-bar`, `flash-volume`, `flash-speed`, `flash-pause-indicator`, `decide-pause-indicator`
There's also a `flash-elements <ids>` message you can use to flash one or more specific elements. Example:
@@ -164,7 +182,7 @@ There's also a `flash-elements <ids>` message you can use to flash one or more s
script-message-to uosc flash-elements timeline,speed
```
Available element IDs: `timeline`, `controls`, `volume`, `top_bar`, `speed`, `pause_indicator`
Available element IDs: `timeline`, `progress`, `controls`, `volume`, `top_bar`, `speed`, `pause_indicator`
This is useful in combination with other commands that modify values represented by flashed elements, for example: flashing volume element when changing the volume.
@@ -204,6 +222,18 @@ Displays a file explorer with directory navigation to load a requested track typ
For subtitles, the explorer only displays file types defined in `subtitle_types` option. For audio and video, the ones defined in `video_types` and `audio_types` are displayed.
#### `download-subtitles`
A menu to search and download subtitles from [Open Subtitles](https://www.opensubtitles.com). It can also be opened by selecting the **Download** option in `subtitles` menu.
We fetch results for languages defined in *uosc**'s `languages` option, which defaults to your mpv `slang` configuration.
We also hash the current file and send the hash to Open Subtitles so you can search even with empty query and if your file is known, you'll get subtitles exactly for it.
Subtitles will be downloaded to the same directory as currently opened file, or `~~/subtitles` (folder in your mpv config directory) if playing a URL.
Current Open Subtitles limit for unauthenticated requests is **5 download per day**, but searching is unlimited. Authentication raises downloads to 10, which doesn't feel like it's worth the effort of implementing it, so currently there's no way to authenticate. 5 downloads per day seems sufficient for most use cases anyway, as if you need more, you should probably just deal with it in the browser beforehand so you don't have to fiddle with the subtitle downloading menu every time you start playing a new file.
#### `playlist`
Playlist navigation.
@@ -220,11 +250,16 @@ Editions menu. Editions are different video cuts available in some mkv files.
Switch stream quality. This is just a basic re-assignment of `ytdl-format` mpv property from predefined options (configurable with `stream_quality_options`) and video reload, there is no fetching of available formats going on.
#### `keybinds`
Displays a command palette menu with all currently active keybindings (defined in your `input.conf` file, or registered by scripts). Useful to check what command is bound to what shortcut, or the other way around.
#### `open-file`
Open file menu. Browsing starts in current file directory, or user directory when file not available. The explorer only displays file types defined in the `video_types`, `audio_types`, and `image_types` options.
You can use `ctrl+enter` or `ctrl+click` to load the whole directory in mpv instead of navigating its contents.
You can use `alt+enter` or `alt+click` to load the whole directory in mpv instead of navigating its contents.
You can also use `ctrl+enter` or `ctrl+click` to append a file or directory to the playlist.
#### `items`
@@ -274,6 +309,24 @@ Switch audio output device.
Open directory with `mpv.conf` in file explorer.
#### `update`
Updates uosc to the latest stable release right from the UI. Available in the "Utils" section of default menu .
Supported environments:
| Env | Works | Note |
|:---|:---:|---|
| Windows | ✔️ | _Not tested on older PowerShell versions. You might need to `Set-ExecutionPolicy` from the install instructions and install with the terminal command first._ |
| Linux (apt) | ✔️ | |
| Linux (flatpak) | ✔️ | |
| Linux (snap) | ❌ | We're not allowed to access commands like `curl` even if they're installed. (Or at least this is what I think the issue is.) |
| MacOS | ❌ | `(23) Failed writing body` error, whatever that means. |
If you know about a solution to fix self-updater for any of the currently broken environments, please make an issue/PR and share it with us!
**Note:** The terminal commands from install instructions still work fine everywhere, so you can use those to update instead.
## Menu
**uosc** provides a way to build, display, and use your own menu. By default it displays a pre-configured menu with common actions.
@@ -343,6 +396,20 @@ Define a folder without defining any of its contents:
# ignore #! Folder title >
```
Define an un-selectable, muted, and italic title item by using `#` as key, and omitting the command:
```
# #! Title
# #! Section > Title
```
Define a separator between previous and next items by doing the same, but using `---` as title:
```
# #! ---
# #! Section > ---
```
Example context menu:
This is the default pre-configured menu if none is defined in your `input.conf`, but with added shortcuts. To both pause & move the window with left mouse button, so that you can have the menu on the right one, enable `click_threshold` in `uosc.conf` (see default `uosc.conf` for example/docs).
@@ -368,14 +435,16 @@ o script-binding uosc/open-file #! Navigation > Open file
# script-binding uosc/audio-device #! Utils > Audio devices
# script-binding uosc/editions #! Utils > Editions
ctrl+s async screenshot #! Utils > Screenshot
alt+i script-binding uosc/keybinds #! Utils > Key bindings
O script-binding uosc/show-in-directory #! Utils > Show in directory
# script-binding uosc/open-config-directory #! Utils > Open config directory
# script-binding uosc/update #! Utils > Update uosc
esc quit #! Quit
```
To see all the commands you can bind keys or menu items to, refer to [mpv's list of input commands documentation](https://mpv.io/manual/master/#list-of-input-commands).
## Message handlers
## Messages
**uosc** listens on some messages that can be sent with `script-message-to uosc` command. Example:
@@ -383,20 +452,6 @@ To see all the commands you can bind keys or menu items to, refer to [mpv's list
R script-message-to uosc show-submenu "Utils > Aspect ratio"
```
### `get-version <script_id>`
Tells uosc to send it's version to `<script_id>` script. Useful if you want to detect that uosc is installed. Example:
```lua
-- Register response handler
mp.register_script_message('uosc-version', function(version)
print('uosc version', version)
end)
-- Ask for version
mp.commandv('script-message-to', 'uosc', 'get-version', mp.get_script_name())
```
### `show-submenu <menu_id>`, `show-submenu-blurred <menu_id>`
Opens one of the submenus defined in `input.conf` (read on how to build those in the Menu documentation above). To prevent 1st item being preselected, use `show-submenu-blurred` instead.
@@ -407,200 +462,56 @@ Parameters
ID (title) of the submenu, including `>` subsections as defined in `input.conf`. It has to be match the title exactly.
### `open-menu <menu_json> [submenu_id]`
## Scripting API
A message other scripts can send to open a uosc menu serialized as JSON. You can optionally pass a `submenu_id` to pre-open a submenu. The ID is the submenu title chain leading to the submenu concatenated with `>`, for example `Tools > Aspect ratio`.
3rd party script developers can use our messaging API to integrate with uosc, or use it to render their menus. Documentation is available in [uosc Wiki](https://github.com/tomasklaen/uosc/wiki).
Menu data structure:
## Contributing
### Localization
If you want to help localizing uosc by either adding a new locale or fixing one that is not up to date, start by running this while in the repository root:
```
Menu {
type?: string;
title?: string;
items: Item[];
selected_index?: integer;
keep_open?: boolean;
on_close: string | string[];
}
Item = Command | Submenu;
Submenu {
title?: string;
hint?: string;
items: Item[];
keep_open?: boolean;
}
Command {
title?: string;
hint?: string;
icon?: string;
value: string | string[];
bold?: boolean;
italic?: boolean;
align?: 'left'|'center'|'right';
selectable?: boolean;
muted?: boolean;
active?: integer;
keep_open?: boolean;
}
tools/intl languagecode
```
When `Command.value` is a string, it'll be passed to `mp.command(value)`. If it's a table (array) of strings, it'll be used as `mp.commandv(table.unpack(value))`. The same goes for `Menu.on_close`.
`languagecode` can be any existing locale in `src/uosc/intl/` directory, or any [IETF language tag](https://en.wikipedia.org/wiki/IETF_language_tag). If it doesn't exist yet, the `intl` tool will create it.
`Menu.type` controls what happens when opening a menu when some other menu is already open. When the new menu type is different, it'll replace the currently opened menu. When it's the same, the currently open menu will simply be closed. This is used to implement toggling of menus with the same type.
This will parse the codebase for localization strings and use them to either update existing locale by removing unused and setting untranslated strings to `null`, or create a new one with all `null` strings.
`item.icon` property accepts icon names. You can pick one from here: [Google Material Icons](https://fonts.google.com/icons?selected=Material+Icons)\
There is also a special icon name `spinner` which will display a rotating spinner. Along with a no-op command on an item and `keep_open=true`, this can be used to display placeholder menus/items that are still loading.
You can then navigate to `src/uosc/intl/languagecode.json` and start translating.
When `keep_open` is `true`, activating the item will not close the menu. This property can be defined on both menus and items, and is inherited from parent to child if child doesn't overwrite it.
### Setting up binaries
It's usually not necessary to define `selected_index` as it'll default to the first `active` item, or 1st item in the list.
Example:
```lua
local utils = require('mp.utils')
local menu = {
type = 'menu_type',
title = 'Custom menu',
items = {
{title = 'Foo', hint = 'foo', value = 'quit'},
{title = 'Bar', hint = 'bar', value = 'quit', active = true},
}
}
local json = utils.format_json(menu)
mp.commandv('script-message-to', 'uosc', 'open-menu', json)
```
### `update-menu <menu_json>`
Updates currently opened menu with the same `type`. If the menu isn't open, it will be opened.
The difference between this and `open-menu` is that if the same type menu is already open, `open-menu` will close it (facilitating menu toggling with the same key/command), while `update-menu` will update it's data.
`update-menu`, along with `{menu/item}.keep_open` property and `item.command` that sends a message back can be used to create a self updating menu with some limited UI. Example:
```lua
local utils = require('mp.utils')
local script_name = mp.get_script_name()
local state = {
checkbox = 'no',
radio = 'bar'
}
function command(str)
return string.format('script-message-to %s %s', script_name, str)
end
function create_menu_data()
return {
type = 'test_menu',
title = 'Test menu',
keep_open = true,
items = {
{
title = 'Checkbox',
icon = state.checkbox == 'yes' and 'check_box' or 'check_box_outline_blank',
value = command('set-state checkbox ' .. (state.checkbox == 'yes' and 'no' or 'yes'))
},
{
title = 'Radio',
hint = state.radio,
items = {
{
title = 'Foo',
icon = state.radio == 'foo' and 'radio_button_checked' or 'radio_button_unchecked',
value = command('set-state radio foo')
},
{
title = 'Bar',
icon = state.radio == 'bar' and 'radio_button_checked' or 'radio_button_unchecked',
value = command('set-state radio bar')
},
{
title = 'Baz',
icon = state.radio == 'baz' and 'radio_button_checked' or 'radio_button_unchecked',
value = command('set-state radio baz')
},
},
},
{
title = 'Submit',
icon = 'check',
value = command('submit'),
keep_open = false
},
}
}
end
mp.add_forced_key_binding('t', 'test_menu', function()
local json = utils.format_json(create_menu_data())
mp.commandv('script-message-to', 'uosc', 'open-menu', json)
end)
mp.register_script_message('set-state', function(prop, value)
state[prop] = value
-- Update currently opened menu
local json = utils.format_json(create_menu_data())
mp.commandv('script-message-to', 'uosc', 'update-menu', json)
end)
mp.register_script_message('submit', function(prop, value)
-- Do something with state
end)
```
### `set <prop> <value>`
Tell **uosc** to set an external property to this value. Currently, this is only used to set/display control button active state and badges:
In your script, set the value of `foo` to `1`.
```lua
mp.commandv('script-message-to', 'uosc', 'set', 'foo', 1)
```
`foo` can now be used as a `toggle` or `cycle` property by specifying its owner with a `@{script_name}` suffix:
If you want to test or work on something that involves ziggy (our multitool binary, currently handles searching & downloading subtitles), you first need to build it with:
```
toggle:icon_name:foo@script_name
cycle:icon_name:foo@script_name:no/yes!
tools/build ziggy
```
If user clicks this `toggle` or `cycle` button, uosc will send a `set` message back to the script owner. You can then listen to this message, do what you need with the new value, and update uosc state accordingly:
This requires [`go`](https://go.dev/dl/) to be installed and in path. If you don't want to bother with installing go, and there were no changes to ziggy, you can just use the binaries from [latest release](https://github.com/tomasklaen/uosc/releases/latest/download/uosc.zip). Place folder `scripts/uosc/bin` from `uosc.zip` into `src/uosc/bin`.
```lua
-- Send initial value so that the button has a correct active state
mp.commandv('script-message-to', 'uosc', 'set', 'foo', 'yes')
-- Listen for changes coming from `toggle` or `cycle` button
mp.register_script_message('set', function(prop, value)
-- ... do something with `value`
-- Update uosc external prop
mp.commandv('script-message-to', 'uosc', 'set', 'foo', value)
end)
```
## FAQ
External properties can also be used as control button badges:
#### Why is the release zip size in megabytes? Isn't this just a lua script?
```
controls=command:icon_name:command_name#foo@script_name?My foo button
```
We are limited in what we can do in mpv's lua scripting environment. To work around this, we include a binary tool (one for each platform), that we call to handle stuff we can't do in lua. Currently this means searching & downloading subtitles, accessing clipboard data, and in future might improve self updating, and potentially other things.
### `overwrite-binding <name> <command>`
Other scripts usually choose to go the route of adding python scripts and requiring users to install the runtime. I don't like this as I want the installation process to be as seamless and as painless as possible. I also don't want to contribute to potential python version mismatch issues, because one tool depends on 2.7, other latest 3, and this one 3.9 only and no newer (real world scenario that happened to me), now have fun reconciling this. Depending on external runtimes can be a mess, and shipping a stable, tiny, and fast binary that users don't even have to know about is imo more preferable than having unstable external dependencies and additional installation steps that force everyone to install and manage hundreds of megabytes big runtimes in global `PATH`.
Allows a overwriting handling of uosc built in bindings. Useful for 3rd party scripts that specialize in a specific domain to replace built in menus or behaviors provided by existing bindings.
#### Why don't you have `uosc-{platform}.zip` releases and only include binaries for the concerned platform in each?
Example that reroutes uosc's basic stream quality menu to [christoph-heinrich/mpv-quality-menu](https://github.com/christoph-heinrich/mpv-quality-menu):
Then you wouldn't be able to sync your mpv config between platforms and everything _just work_.
```lua
mp.commandv('script-message-to', 'uosc', 'overwrite-binding', 'stream-quality', 'script-binding quality_menu/video_formats_toggle')
```
#### Why is the release reported as malicious by some antiviruses?
To cancel the overwrite and return to default behavior, just omit the `<command>` parameter.
Some antiviruses find our binaries suspicious due to the way go packages them. This is a known issue with all go binaries (https://go.dev/doc/faq#virus). I think the only way to solve that would be to sign them (not 100% sure though), but I'm not paying to work on free stuff. If anyone is bothered by this, and would be willing to donate a code signing certificate, let me know.
## Why _uosc_?
If you want to check the binaries are safe, the code is in `src/ziggy`, and you can build them yourself by running `tools/build ziggy` in the repository root.
We might eventually rewrite it in something else.
#### Why _uosc_?
It stood for micro osc as it used to render just a couple rectangles before it grew to what it is today. And now it means a minimalist UI design direction where everything is out of your way until needed.

9
go.mod Normal file
View File

@@ -0,0 +1,9 @@
module uosc/bins
go 1.21.3
require (
github.com/atotto/clipboard v0.1.4
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
k8s.io/apimachinery v0.28.3
)

6
go.sum Normal file
View File

@@ -0,0 +1,6 @@
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
k8s.io/apimachinery v0.28.3 h1:B1wYx8txOaCQG0HmYF6nbpU8dg6HvA06x5tEffvOe7A=
k8s.io/apimachinery v0.28.3/go.mod h1:uQTKmIqs+rAYaq+DFaoD2X7pcjLOqbQX2AOiO0nIpb8=

123
installers/unix.sh Normal file
View File

@@ -0,0 +1,123 @@
#!/usr/bin/env bash
zip_url=https://github.com/tomasklaen/uosc/releases/latest/download/uosc.zip
conf_url=https://github.com/tomasklaen/uosc/releases/latest/download/uosc.conf
zip_file=/tmp/uosc.zip
files=("scripts/uosc" "fonts/uosc_icons.otf" "fonts/uosc_textures.ttf" "scripts/uosc_shared" "scripts/uosc.lua")
dependencies=(curl unzip)
# Exit immediately if a command exits with a non-zero status
set -e
abort() {
echo "Error: $1"
echo "Aborting!"
rm -f $zip_file || true
echo "Deleting potentially broken install..."
for file in ${files[@]}
do
rm -rf "$config_dir/$file" || true
done
echo "Restoring backup..."
for file in ${files[@]}
do
from_path="$backup_dir/$file"
if [[ -e "$from_path" ]]; then
to_path="$config_dir/$file"
to_dir="$(dirname "${to_path}")"
mkdir -pv $to_dir || true
mv $from_path $to_path || true
fi
done
echo "Deleting backup..."
rm -rf $backup_dir || true
exit 1
}
# Check dependencies
missing_dependencies=()
for name in ${dependencies[@]}
do
if [ ! -x "$(command -v $name)" ]; then
missing_dependencies+=($name)
fi
done
if [ ! ${#missing_dependencies[@]} -eq 0 ]; then
echo "Missing dependencies: ${missing_dependencies[@]}"
exit 1
fi
# Determine install directory
OS="$(uname)"
if [ ! -z "${MPV_CONFIG_DIR}" ]; then
echo "Installing into (MPV_CONFIG_DIR):"
config_dir="${MPV_CONFIG_DIR}"
elif [ "${OS}" == "Linux" ]; then
# Flatpak
if [ -d "$HOME/.var/app/io.mpv.Mpv" ]; then
echo "Installing into (flatpak io.mpv.Mpv package):"
config_dir="$HOME/.var/app/io.mpv.Mpv/config/mpv"
# Snap mpv
elif [ -d "$HOME/snap/mpv" ]; then
echo "Installing into (snap mpv package):"
config_dir="$HOME/snap/mpv/current/.config/mpv"
# Snap mpv-wayland
elif [ -d "$HOME/snap/mpv-wayland" ]; then
echo "Installing into (snap mpv-wayland package):"
config_dir="$HOME/snap/mpv-wayland/common/.config/mpv"
# ~/.config
else
echo "Config location:"
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/mpv"
fi
elif [ "${OS}" == "Darwin" ]; then
config_dir=~/.config/mpv
else
abort "This install script works only on Linux and macOS."
fi
backup_dir="$config_dir/.uosc-backup"
echo "$config_dir"
mkdir -p $config_dir || abort "Couldn't create config directory."
echo "Backing up..."
rm -rf $backup_dir || abort "Couldn't cleanup backup directory."
for file in ${files[@]}
do
from_path="$config_dir/$file"
if [[ -e "$from_path" ]]; then
to_path="$backup_dir/$file"
to_dir="$(dirname "${to_path}")"
mkdir -p $to_dir || abort "Couldn't create backup folder: $to_dir"
mv $from_path $to_path || abort "Couldn't move '$from_path' to '$to_path'."
fi
done
# Install new version
echo "Downloading archive..."
curl -Ls -o $zip_file $zip_url || abort "Couldn't download: $zip_url"
echo "Extracting archive..."
unzip -qod $config_dir $zip_file || abort "Couldn't extract: $zip_file"
echo "Deleting archive..."
rm -f $zip_file || echo "Couldn't delete: $zip_file"
echo "Deleting backup..."
rm -rf $backup_dir || echo "Couldn't delete: $backup_dir"
# Download default config if one doesn't exist yet
scriptopts_dir="$config_dir/script-opts"
conf_file="$scriptopts_dir/uosc.conf"
if [ ! -f "$conf_file" ]; then
echo "Config not found, downloading default uosc.conf..."
mkdir -p $scriptopts_dir || echo "Couldn't create: $scriptopts_dir"
curl -Ls -o $conf_file $conf_url || echo "Couldn't download: $conf_url"
fi
echo "uosc has been installed."

134
installers/windows.ps1 Normal file
View File

@@ -0,0 +1,134 @@
$ZipURL = "https://github.com/tomasklaen/uosc/releases/latest/download/uosc.zip"
$ConfURL = "https://github.com/tomasklaen/uosc/releases/latest/download/uosc.conf"
$Files = "scripts/uosc", "fonts/uosc_icons.otf", "fonts/uosc_textures.ttf", "scripts/uosc_shared", "scripts/uosc.lua"
# Determine install directory
if (Test-Path env:MPV_CONFIG_DIR) {
Write-Output "Installing into (MPV_CONFIG_DIR):"
$ConfigDir = "$env:MPV_CONFIG_DIR"
}
elseif (Test-Path "$PWD/portable_config") {
Write-Output "Installing into (portable config):"
$ConfigDir = "$PWD/portable_config"
}
elseif ((Get-Item -Path $PWD).BaseName -eq "portable_config") {
Write-Output "Installing into (portable config):"
$ConfigDir = "$PWD"
}
else {
Write-Output "Installing into (current user config):"
$ConfigDir = "$env:APPDATA/mpv"
if (!(Test-Path $ConfigDir)) {
Write-Output "Creating folder: $ConfigDir"
New-Item -ItemType Directory -Force -Path $ConfigDir > $null
}
}
Write-Output "$ConfigDir"
$BackupDir = "$ConfigDir/.uosc-backup"
$ZipFile = "$ConfigDir/uosc_tmp.zip"
function DeleteIfExists($Path) {
if (Test-Path $Path) {
Remove-Item -LiteralPath $Path -Force -Recurse > $null
}
}
Function Abort($Message) {
Write-Output "Error: $Message"
Write-Output "Aborting!"
DeleteIfExists($ZipFile)
Write-Output "Deleting potentially broken install..."
foreach ($File in $Files) {
DeleteIfExists("$ConfigDir/$File")
}
Write-Output "Restoring backup..."
foreach ($File in $Files) {
$FromPath = "$BackupDir/$File"
if (Test-Path $FromPath) {
$ToPath = "$ConfigDir/$File"
$ToDir = Split-Path $ToPath -parent
New-Item -ItemType Directory -Force -Path $ToDir > $null
Move-Item -LiteralPath $FromPath -Destination $ToPath -Force > $null
}
}
Write-Output "Deleting backup..."
DeleteIfExists($BackupDir)
Exit 1
}
# Ensure install directory exists
if (!(Test-Path -Path $ConfigDir -PathType Container)) {
if (Test-Path -Path $ConfigDir -PathType Leaf) {
Abort("Config directory is a file.")
}
try {
New-Item -ItemType Directory -Force -Path $ConfigDir > $null
}
catch {
Abort("Couldn't create config directory.")
}
}
Write-Output "Backing up..."
foreach ($File in $Files) {
$FromPath = "$ConfigDir/$File"
if (Test-Path $FromPath) {
$ToPath = "$BackupDir/$File"
$ToDir = Split-Path $ToPath -parent
try {
New-Item -ItemType Directory -Force -Path $ToDir > $null
}
catch {
Abort("Couldn't create backup folder: $ToDir")
}
try {
Move-Item -LiteralPath $FromPath -Destination $ToPath -Force > $null
}
catch {
Abort("Couldn't move '$FromPath' to '$ToPath'.")
}
}
}
# Install new version
Write-Output "Downloading archive..."
try {
Invoke-WebRequest -OutFile $ZipFile -Uri $ZipURL > $null
}
catch {
Abort("Couldn't download: $ZipURL")
}
Write-Output "Extracting archive..."
try {
Expand-Archive $ZipFile -DestinationPath $ConfigDir -Force > $null
}
catch {
Abort("Couldn't extract: $ZipFile")
}
Write-Output "Deleting archive..."
DeleteIfExists($ZipFile)
Write-Output "Deleting backup..."
DeleteIfExists($BackupDir)
# Download default config if one doesn't exist yet
try {
$ScriptOptsDir = "$ConfigDir/script-opts"
$ConfFile = "$ScriptOptsDir/uosc.conf"
if (!(Test-Path $ConfFile)) {
Write-Output "Config not found, downloading default uosc.conf..."
New-Item -ItemType Directory -Force -Path $ScriptOptsDir > $null
Invoke-WebRequest -OutFile $ConfFile -Uri $ConfURL > $null
}
}
catch {
Abort("Couldn't download the config file, but uosc should be installed correctly.")
}
Write-Output "uosc has been installed."

View File

@@ -1,906 +0,0 @@
local Element = require('elements/Element')
-- Menu data structure accepted by `Menu:open(menu)`.
---@alias MenuData {type?: string; title?: string; hint?: string; keep_open?: boolean; separator?: boolean; items?: MenuDataItem[]; selected_index?: integer;}
---@alias MenuDataItem MenuDataValue|MenuData
---@alias MenuDataValue {title?: string; hint?: string; icon?: string; value: any; bold?: boolean; italic?: boolean; muted?: boolean; active?: boolean; keep_open?: boolean; separator?: boolean; selectable?: boolean; align?: 'left'|'center'|'right'}
---@alias MenuOptions {mouse_nav?: boolean; on_open?: fun(); on_close?: fun(); on_back?: fun(); on_move_item?: fun(from_index: integer, to_index: integer, submenu_path: integer[]); on_delete_item?: fun(index: integer, submenu_path: integer[])}
-- Internal data structure created from `Menu`.
---@alias MenuStack {id?: string; type?: string; title?: string; hint?: string; selected_index?: number; keep_open?: boolean; separator?: boolean; items: MenuStackItem[]; parent_menu?: MenuStack; submenu_path: integer[]; active?: boolean; width: number; height: number; top: number; scroll_y: number; scroll_height: number; title_width: number; hint_width: number; max_width: number; is_root?: boolean; fling?: Fling}
---@alias MenuStackItem MenuStackValue|MenuStack
---@alias MenuStackValue {title?: string; hint?: string; icon?: string; value: any; active?: boolean; bold?: boolean; italic?: boolean; muted?: boolean; keep_open?: boolean; separator?: boolean; selectable?: boolean; align?: 'left'|'center'|'right'; title_width: number; hint_width: number}
---@alias Fling {y: number, distance: number, time: number, easing: fun(x: number), duration: number, update_cursor?: boolean}
---@alias Modifiers {shift?: boolean, ctrl?: boolean, alt?: boolean}
---@alias MenuCallbackMeta {modifiers: Modifiers}
---@alias MenuCallback fun(value: any, meta: MenuCallbackMeta)
---@class Menu : Element
local Menu = class(Element)
---@param data MenuData
---@param callback MenuCallback
---@param opts? MenuOptions
function Menu:open(data, callback, opts)
local open_menu = self:is_open()
if open_menu then
open_menu.is_being_replaced = true
open_menu:close(true)
end
return Menu:new(data, callback, opts)
end
---@param menu_type? string
---@return Menu|nil
function Menu:is_open(menu_type)
return Elements.menu and (not menu_type or Elements.menu.type == menu_type) and Elements.menu or nil
end
---@param immediate? boolean Close immediately without fadeout animation.
---@param callback? fun() Called after the animation (if any) ends and element is removed and destroyed.
---@overload fun(callback: fun())
function Menu:close(immediate, callback)
if type(immediate) ~= 'boolean' then callback = immediate end
local menu = self == Menu and Elements.menu or self
if menu and not menu.destroyed then
if menu.is_closing then
menu:tween_stop()
return
end
local function close()
Elements:remove('menu')
menu.is_closing, menu.stack, menu.current, menu.all, menu.by_id = false, nil, nil, {}, {}
menu:disable_key_bindings()
Elements:update_proximities()
cursor.queue_autohide()
if callback then callback() end
request_render()
end
menu.is_closing = true
if immediate then close()
else menu:fadeout(close) end
end
end
---@param data MenuData
---@param callback MenuCallback
---@param opts? MenuOptions
---@return Menu
function Menu:new(data, callback, opts) return Class.new(self, data, callback, opts) --[[@as Menu]] end
---@param data MenuData
---@param callback MenuCallback
---@param opts? MenuOptions
function Menu:init(data, callback, opts)
Element.init(self, 'menu', {ignores_menu = true})
-----@type fun()
self.callback = callback
self.opts = opts or {}
self.offset_x = 0 -- Used for submenu transition animation.
self.mouse_nav = self.opts.mouse_nav -- Stops pre-selecting items
---@type Modifiers|nil
self.modifiers = nil
self.item_height = nil
self.item_spacing = 1
self.item_padding = nil
self.font_size = nil
self.font_size_hint = nil
self.scroll_step = nil -- Item height + item spacing.
self.scroll_height = nil -- Items + spacings - container height.
self.opacity = 0 -- Used to fade in/out.
self.type = data.type
---@type MenuStack Root MenuStack.
self.root = nil
---@type MenuStack Current MenuStack.
self.current = nil
---@type MenuStack[] All menus in a flat array.
self.all = nil
---@type table<string, MenuStack> Map of submenus by their ids, such as `'Tools > Aspect ratio'`.
self.by_id = {}
self.key_bindings = {}
self.is_being_replaced = false
self.is_closing, self.is_closed = false, false
---@type {y: integer, time: number}[]
self.drag_data = nil
self.is_dragging = false
self:update(data)
if self.mouse_nav then
if self.current then self.current.selected_index = nil end
else
for _, menu in ipairs(self.all) do self:scroll_to_index(menu.selected_index, menu) end
end
self:tween_property('opacity', 0, 1)
self:enable_key_bindings()
Elements.curtain:register('menu')
if self.opts.on_open then self.opts.on_open() end
end
function Menu:destroy()
Element.destroy(self)
self:disable_key_bindings()
self.is_closed = true
if not self.is_being_replaced then Elements.curtain:unregister('menu') end
if self.opts.on_close then self.opts.on_close() end
end
---@param data MenuData
function Menu:update(data)
self.type = data.type
local new_root = {is_root = true, submenu_path = {}}
local new_all = {}
local new_by_id = {}
local menus_to_serialize = {{new_root, data}}
local old_current_id = self.current and self.current.id
table_assign(new_root, data, {'type', 'title', 'hint', 'keep_open'})
local i = 0
while i < #menus_to_serialize do
i = i + 1
local menu, menu_data = menus_to_serialize[i][1], menus_to_serialize[i][2]
local parent_id = menu.parent_menu and not menu.parent_menu.is_root and menu.parent_menu.id
if not menu.is_root then
menu.id = (parent_id and parent_id .. ' > ' or '') .. (menu_data.title or i)
end
menu.icon = 'chevron_right'
-- Update items
local first_active_index = nil
menu.items = {
{title = t('Empty'), value = 'ignore', italic = 'true', muted = 'true', selectable = false, align = 'center'}
}
for i, item_data in ipairs(menu_data.items or {}) do
if item_data.active and not first_active_index then first_active_index = i end
local item = {}
table_assign(item, item_data, {
'title', 'icon', 'hint', 'active', 'bold', 'italic', 'muted', 'value', 'keep_open', 'separator',
'selectable', 'align'
})
if item.keep_open == nil then item.keep_open = menu.keep_open end
-- Submenu
if item_data.items then
item.parent_menu = menu
item.submenu_path = itable_join(menu.submenu_path, {i})
menus_to_serialize[#menus_to_serialize + 1] = {item, item_data}
end
menu.items[i] = item
end
if menu.is_root then menu.selected_index = menu_data.selected_index or first_active_index end
-- Retain old state
local old_menu = self.by_id[menu.is_root and '__root__' or menu.id]
if old_menu then table_assign(menu, old_menu, {'selected_index', 'scroll_y', 'fling'}) end
if menu.selected_index then
menu.selected_index = #menu.items > 0 and clamp(1, menu.selected_index, #menu.items) or nil
end
new_all[#new_all + 1] = menu
new_by_id[menu.is_root and '__root__' or menu.id] = menu
end
self.root, self.all, self.by_id = new_root, new_all, new_by_id
self.current = self.by_id[old_current_id] or self.root
self:update_content_dimensions()
self:reset_navigation()
end
---@param items MenuDataItem[]
function Menu:update_items(items)
local data = table_shallow_copy(self.root)
data.items = items
self:update(data)
end
function Menu:update_content_dimensions()
self.item_height = state.fullormaxed and options.menu_item_height_fullscreen or options.menu_item_height
self.font_size = round(self.item_height * 0.48 * options.font_scale)
self.font_size_hint = self.font_size - 1
self.item_padding = round((self.item_height - self.font_size) * 0.6)
self.scroll_step = self.item_height + self.item_spacing
local title_opts = {size = self.font_size, italic = false, bold = false}
local hint_opts = {size = self.font_size_hint}
for _, menu in ipairs(self.all) do
title_opts.bold, title_opts.italic = true, false
local max_width = text_width(menu.title, title_opts) + 2 * self.item_padding
-- Estimate width of a widest item
for _, item in ipairs(menu.items) do
local icon_width = item.icon and self.font_size or 0
item.title_width = text_width(item.title, title_opts)
item.hint_width = text_width(item.hint, hint_opts)
local spacings_in_item = 1 + (item.title_width > 0 and 1 or 0)
+ (item.hint_width > 0 and 1 or 0) + (icon_width > 0 and 1 or 0)
local estimated_width = item.title_width + item.hint_width + icon_width
+ (self.item_padding * spacings_in_item)
if estimated_width > max_width then max_width = estimated_width end
end
menu.max_width = max_width
end
self:update_dimensions()
end
function Menu:update_dimensions()
-- Coordinates and sizes are of the scrollable area to make
-- consuming values in rendering and collisions easier. Title is rendered
-- above it, so we need to account for that in max_height and ay position.
local min_width = state.fullormaxed and options.menu_min_width_fullscreen or options.menu_min_width
for _, menu in ipairs(self.all) do
menu.width = round(clamp(min_width, menu.max_width, display.width * 0.9))
local title_height = (menu.is_root and menu.title) and self.scroll_step or 0
local max_height = round((display.height - title_height) * 0.9)
local content_height = self.scroll_step * #menu.items
menu.height = math.min(content_height - self.item_spacing, max_height)
menu.top = round(math.max((display.height - menu.height) / 2, title_height * 1.5))
menu.scroll_height = math.max(content_height - menu.height - self.item_spacing, 0)
menu.scroll_y = menu.scroll_y or 0
self:scroll_to(menu.scroll_y, menu) -- clamps scroll_y to scroll limits
end
self:update_coordinates()
end
-- Updates element coordinates to match currently open (sub)menu.
function Menu:update_coordinates()
local ax = round((display.width - self.current.width) / 2) + self.offset_x
self:set_coordinates(ax, self.current.top, ax + self.current.width, self.current.top + self.current.height)
end
function Menu:reset_navigation()
local menu = self.current
-- Reset indexes and scroll
self:scroll_to(menu.scroll_y) -- clamps scroll_y to scroll limits
if menu.items and #menu.items > 0 then
-- Normalize existing selected_index always, and force it only in keyboard navigation
if not self.mouse_nav and not menu.selected_index then
local from = clamp(1, menu.selected_index or 1, #menu.items)
self:select_index(itable_find(menu.items, function(item) return item.selectable ~= false end, from), menu)
end
else
self:select_index(nil)
end
-- Walk up the parent menu chain and activate items that lead to current menu
local parent = menu.parent_menu
while parent do
parent.selected_index = itable_index_of(parent.items, menu)
menu, parent = parent, parent.parent_menu
end
request_render()
end
function Menu:set_offset_x(offset)
local delta = offset - self.offset_x
self.offset_x = offset
self:set_coordinates(self.ax + delta, self.ay, self.bx + delta, self.by)
end
function Menu:fadeout(callback) self:tween_property('opacity', 1, 0, callback) end
function Menu:get_first_active_index(menu)
menu = menu or self.current
for index, item in ipairs(self.current.items) do
if item.active then return index end
end
end
---@param pos? number
---@param menu? MenuStack
function Menu:set_scroll_to(pos, menu)
menu = menu or self.current
menu.scroll_y = clamp(0, pos or 0, menu.scroll_height)
request_render()
end
---@param delta? number
---@param menu? MenuStack
function Menu:set_scroll_by(delta, menu)
menu = menu or self.current
self:set_scroll_to(menu.scroll_y + delta, menu)
end
---@param pos? number
---@param menu? MenuStack
---@param fling_options? table
function Menu:scroll_to(pos, menu, fling_options)
menu = menu or self.current
menu.fling = {
y = menu.scroll_y, distance = clamp(-menu.scroll_y, pos - menu.scroll_y, menu.scroll_height - menu.scroll_y),
time = mp.get_time(), duration = 0.1, easing = ease_out_sext,
}
if fling_options then table_assign(menu.fling, fling_options) end
request_render()
end
---@param delta? number
---@param menu? MenuStack
---@param fling_options? Fling
function Menu:scroll_by(delta, menu, fling_options)
menu = menu or self.current
self:scroll_to((menu.fling and (menu.fling.y + menu.fling.distance) or menu.scroll_y) + delta, menu, fling_options)
end
---@param index? integer
---@param menu? MenuStack
---@param immediate? boolean
function Menu:scroll_to_index(index, menu, immediate)
menu = menu or self.current
if (index and index >= 1 and index <= #menu.items) then
local position = round((self.scroll_step * (index - 1)) - ((menu.height - self.scroll_step) / 2))
if immediate then self:set_scroll_to(position, menu)
else self:scroll_to(position, menu) end
end
end
---@param index? integer
---@param menu? MenuStack
function Menu:select_index(index, menu)
menu = menu or self.current
menu.selected_index = (index and index >= 1 and index <= #menu.items) and index or nil
request_render()
end
---@param value? any
---@param menu? MenuStack
function Menu:select_value(value, menu)
menu = menu or self.current
local index = itable_find(menu.items, function(item) return item.value == value end)
self:select_index(index)
end
---@param menu? MenuStack
function Menu:deactivate_items(menu)
menu = menu or self.current
for _, item in ipairs(menu.items) do item.active = false end
request_render()
end
---@param index? integer
---@param menu? MenuStack
function Menu:activate_index(index, menu)
menu = menu or self.current
if index and index >= 1 and index <= #menu.items then menu.items[index].active = true end
request_render()
end
---@param index? integer
---@param menu? MenuStack
function Menu:activate_one_index(index, menu)
self:deactivate_items(menu)
self:activate_index(index, menu)
end
---@param value? any
---@param menu? MenuStack
function Menu:activate_value(value, menu)
menu = menu or self.current
local index = itable_find(menu.items, function(item) return item.value == value end)
self:activate_index(index, menu)
end
---@param value? any
---@param menu? MenuStack
function Menu:activate_one_value(value, menu)
menu = menu or self.current
local index = itable_find(menu.items, function(item) return item.value == value end)
self:activate_one_index(index, menu)
end
---@param menu MenuStack One of menus in `self.all`.
function Menu:activate_menu(menu)
if itable_index_of(self.all, menu) then
self.current = menu
self:update_coordinates()
self:reset_navigation()
request_render()
else
msg.error('Attempt to open a menu not in `self.all` list.')
end
end
---@param id string
function Menu:activate_submenu(id)
local submenu = self.by_id[id]
if submenu then self:activate_menu(submenu)
else msg.error(string.format('Requested submenu id "%s" doesn\'t exist', id)) end
end
---@param index? integer
---@param menu? MenuStack
function Menu:delete_index(index, menu)
menu = menu or self.current
if (index and index >= 1 and index <= #menu.items) then
table.remove(menu.items, index)
self:update_content_dimensions()
self:scroll_to_index(menu.selected_index, menu)
end
end
---@param value? any
---@param menu? MenuStack
function Menu:delete_value(value, menu)
menu = menu or self.current
local index = itable_find(menu.items, function(item) return item.value == value end)
self:delete_index(index)
end
---@param menu? MenuStack
function Menu:prev(menu)
menu = menu or self.current
local initial_index = menu.selected_index and menu.selected_index - 1 or #menu.items
if initial_index > 0 then
menu.selected_index = itable_find(menu.items, function(item) return item.selectable ~= false end, initial_index, 1)
self:scroll_to_index(menu.selected_index, menu, true)
end
end
---@param menu? MenuStack
function Menu:next(menu)
menu = menu or self.current
local initial_index = menu.selected_index and menu.selected_index + 1 or 1
if initial_index <= #menu.items then
menu.selected_index = itable_find(menu.items, function(item) return item.selectable ~= false end, initial_index)
self:scroll_to_index(menu.selected_index, menu, true)
end
end
---@param menu MenuStack One of menus in `self.all`.
---@param x number `x` coordinate to slide from.
function Menu:slide_in_menu(menu, x)
local current = self.current
current.selected_index = nil
self:activate_menu(menu)
self:tween(-(display.width / 2 - menu.width / 2 - x), 0, function(offset) self:set_offset_x(offset) end)
self.opacity = 1 -- in case tween above canceled fade in animation
end
function Menu:back()
if self.opts.on_back then
self.opts.on_back()
if self.is_closed then return end
end
local current = self.current
local parent = current.parent_menu
if parent then
self:slide_in_menu(parent, display.width / 2 - current.width / 2 - parent.width / 2 + self.offset_x)
else
self:close()
end
end
---@param opts? {keep_open?: boolean, preselect_first_item?: boolean}
function Menu:open_selected_item(opts)
opts = opts or {}
local menu = self.current
if menu.selected_index then
local item = menu.items[menu.selected_index]
-- Is submenu
if item.items then
if opts.preselect_first_item then
item.selected_index = #item.items > 0 and 1 or nil
end
self:activate_menu(item)
self:tween(self.offset_x + menu.width / 2, 0, function(offset) self:set_offset_x(offset) end)
self.opacity = 1 -- in case tween above canceled fade in animation
else
self.callback(item.value, {modifiers = self.modifiers or {}})
if not item.keep_open and not opts.keep_open then self:close() end
end
end
end
function Menu:open_selected_item_soft() self:open_selected_item({keep_open = true}) end
function Menu:open_selected_item_preselect() self:open_selected_item({preselect_first_item = true}) end
---@param index integer
function Menu:move_selected_item_to(index)
local from, callback = self.current.selected_index, self.opts.on_move_item
if callback and from and from ~= index and index >= 1 and index <= #self.current.items then
callback(from, index, self.current.submenu_path)
self.current.selected_index = index
self:set_scroll_by((index - from) * self.scroll_step)
end
end
function Menu:move_selected_item_up()
if self.current.selected_index then self:move_selected_item_to(self.current.selected_index - 1) end
end
function Menu:move_selected_item_down()
if self.current.selected_index then self:move_selected_item_to(self.current.selected_index + 1) end
end
function Menu:delete_selected_item()
local index, callback = self.current.selected_index, self.opts.on_delete_item
if callback and index then callback(index, self.current.submenu_path) end
end
function Menu:on_display() self:update_dimensions() end
function Menu:on_prop_fullormaxed() self:update_content_dimensions() end
function Menu:handle_cursor_down()
if self.proximity_raw == 0 then
self.drag_data = {{y = cursor.y, time = mp.get_time()}}
self.current.fling = nil
else
self:close()
end
end
function Menu:fling_distance()
local first, last = self.drag_data[1], self.drag_data[#self.drag_data]
if mp.get_time() - last.time > 0.05 then return 0 end
for i = #self.drag_data - 1, 1, -1 do
local drag = self.drag_data[i]
if last.time - drag.time > 0.03 then return ((drag.y - last.y) / ((last.time - drag.time) / 0.03)) * 10 end
end
return #self.drag_data < 2 and 0 or ((first.y - last.y) / ((first.time - last.time) / 0.03)) * 10
end
function Menu:handle_cursor_up()
if self.proximity_raw == 0 and self.drag_data and not self.is_dragging then
self:open_selected_item({preselect_first_item = false, keep_open = self.modifiers and self.modifiers.shift})
end
if self.is_dragging then
local distance = self:fling_distance()
if math.abs(distance) > 50 then
self.current.fling = {
y = self.current.scroll_y, distance = distance, time = self.drag_data[#self.drag_data].time,
easing = ease_out_quart, duration = 0.5, update_cursor = true,
}
end
end
self.is_dragging = false
self.drag_data = nil
end
function Menu:on_global_mouse_move()
self.mouse_nav = true
if self.drag_data then
self.is_dragging = self.is_dragging or math.abs(cursor.y - self.drag_data[1].y) >= 10
local distance = self.drag_data[#self.drag_data].y - cursor.y
if distance ~= 0 then self:set_scroll_by(distance) end
self.drag_data[#self.drag_data + 1] = {y = cursor.y, time = mp.get_time()}
end
request_render()
end
function Menu:handle_wheel_up() self:scroll_by(self.scroll_step * -3, nil, {update_cursor = true}) end
function Menu:handle_wheel_down() self:scroll_by(self.scroll_step * 3, nil, {update_cursor = true}) end
function Menu:on_pgup()
local menu = self.current
local items_per_page = round((menu.height / self.scroll_step) * 0.4)
local paged_index = (menu.selected_index and menu.selected_index or #menu.items) - items_per_page
menu.selected_index = clamp(1, paged_index, #menu.items)
if menu.selected_index > 0 then self:scroll_to_index(menu.selected_index) end
end
function Menu:on_pgdwn()
local menu = self.current
local items_per_page = round((menu.height / self.scroll_step) * 0.4)
local paged_index = (menu.selected_index and menu.selected_index or 1) + items_per_page
menu.selected_index = clamp(1, paged_index, #menu.items)
if menu.selected_index > 0 then self:scroll_to_index(menu.selected_index) end
end
function Menu:on_home()
self.current.selected_index = math.min(1, #self.current.items)
if self.current.selected_index > 0 then self:scroll_to_index(self.current.selected_index) end
end
function Menu:on_end()
self.current.selected_index = #self.current.items
if self.current.selected_index > 0 then self:scroll_to_index(self.current.selected_index) end
end
function Menu:add_key_binding(key, name, fn, flags)
self.key_bindings[#self.key_bindings + 1] = name
mp.add_forced_key_binding(key, name, fn, flags)
end
function Menu:enable_key_bindings()
-- The `mp.set_key_bindings()` method would be easier here, but that
-- doesn't support 'repeatable' flag, so we are stuck with this monster.
self:add_key_binding('up', 'menu-prev1', self:create_key_action('prev'), 'repeatable')
self:add_key_binding('down', 'menu-next1', self:create_key_action('next'), 'repeatable')
self:add_key_binding('ctrl+up', 'menu-move-up', self:create_key_action('move_selected_item_up'), 'repeatable')
self:add_key_binding('ctrl+down', 'menu-move-down', self:create_key_action('move_selected_item_down'), 'repeatable')
self:add_key_binding('left', 'menu-back1', self:create_key_action('back'))
self:add_key_binding('right', 'menu-select1', self:create_key_action('open_selected_item_preselect'))
self:add_key_binding('shift+right', 'menu-select-soft1',
self:create_key_action('open_selected_item_soft', {shift = true}))
self:add_key_binding('shift+mbtn_left', 'menu-select3', self:create_modified_mbtn_left_handler({shift = true}))
self:add_key_binding('ctrl+mbtn_left', 'menu-select4', self:create_modified_mbtn_left_handler({ctrl = true}))
self:add_key_binding('mbtn_back', 'menu-back-alt3', self:create_key_action('back'))
self:add_key_binding('bs', 'menu-back-alt4', self:create_key_action('back'))
self:add_key_binding('enter', 'menu-select-alt3', self:create_key_action('open_selected_item_preselect'))
self:add_key_binding('kp_enter', 'menu-select-alt4', self:create_key_action('open_selected_item_preselect'))
self:add_key_binding('ctrl+enter', 'menu-select-ctrl1',
self:create_key_action('open_selected_item_preselect', {ctrl = true}))
self:add_key_binding('ctrl+kp_enter', 'menu-select-ctrl2',
self:create_key_action('open_selected_item_preselect', {ctrl = true}))
self:add_key_binding('shift+enter', 'menu-select-alt5',
self:create_key_action('open_selected_item_soft', {shift = true}))
self:add_key_binding('shift+kp_enter', 'menu-select-alt6',
self:create_key_action('open_selected_item_soft', {shift = true}))
self:add_key_binding('esc', 'menu-close', self:create_key_action('close'))
self:add_key_binding('pgup', 'menu-page-up', self:create_key_action('on_pgup'), 'repeatable')
self:add_key_binding('pgdwn', 'menu-page-down', self:create_key_action('on_pgdwn'), 'repeatable')
self:add_key_binding('home', 'menu-home', self:create_key_action('on_home'))
self:add_key_binding('end', 'menu-end', self:create_key_action('on_end'))
self:add_key_binding('del', 'menu-delete-item', self:create_key_action('delete_selected_item'))
end
function Menu:disable_key_bindings()
for _, name in ipairs(self.key_bindings) do mp.remove_key_binding(name) end
self.key_bindings = {}
end
---@param modifiers Modifiers
function Menu:create_modified_mbtn_left_handler(modifiers)
return function()
self.mouse_nav = true
self.modifiers = modifiers
self:handle_cursor_down()
self:handle_cursor_up()
self.modifiers = nil
end
end
---@param name string
---@param modifiers? Modifiers
function Menu:create_key_action(name, modifiers)
return function()
self.mouse_nav = false
self.modifiers = modifiers
self:maybe(name)
self.modifiers = nil
end
end
function Menu:render()
for _, menu in ipairs(self.all) do
if menu.fling then
local time_delta = state.render_last_time - menu.fling.time
local progress = menu.fling.easing(math.min(time_delta / menu.fling.duration, 1))
self:set_scroll_to(round(menu.fling.y + menu.fling.distance * progress), menu)
if progress < 1 then request_render() else menu.fling = nil end
end
end
cursor.on_primary_down = function() self:handle_cursor_down() end
cursor.on_primary_up = function() self:handle_cursor_up() end
if self.proximity_raw == 0 then
cursor.on_wheel_down = function() self:handle_wheel_down() end
cursor.on_wheel_up = function() self:handle_wheel_up() end
end
local ass = assdraw.ass_new()
local opacity = options.menu_opacity * self.opacity
local spacing = self.item_padding
local icon_size = self.font_size
local menu_gap, menu_padding = 2, 2
---@param menu MenuStack
---@param x number
---@param pos number Horizontal position index. 0 = current menu, <0 parent menus, >1 submenu.
local function draw_menu(menu, x, pos)
local is_current, is_parent, is_submenu = pos == 0, pos < 0, pos > 0
local menu_opacity = pos == 0 and opacity or opacity * (options.menu_parent_opacity ^ math.abs(pos))
local ax, ay, bx, by = x, menu.top, x + menu.width, menu.top + menu.height
local draw_title = menu.is_root and menu.title
local scroll_clip = '\\clip(0,' .. ay .. ',' .. display.width .. ',' .. by .. ')'
local start_index = math.floor(menu.scroll_y / self.scroll_step) + 1
local end_index = math.ceil((menu.scroll_y + menu.height) / self.scroll_step)
-- Remove menu_opacity to start off with full, but still decay for parent menus
local text_opacity = menu_opacity / options.menu_opacity
local menu_rect = {ax = ax, ay = ay - (draw_title and self.item_height or 0) - 2, bx = bx, by = by + 2}
local blur_selected_index = is_current and self.mouse_nav
-- Background
ass:rect(menu_rect.ax, menu_rect.ay, menu_rect.bx, menu_rect.by, {color = bg, opacity = menu_opacity, radius = 4})
if is_parent and get_point_to_rectangle_proximity(cursor, menu_rect) == 0 then
cursor.on_primary_down = function() self:slide_in_menu(menu, x) end
end
-- Draw submenu if selected
local submenu_rect, current_item = nil, is_current and menu.selected_index and menu.items[menu.selected_index]
local submenu_is_hovered = false
if current_item and current_item.items then
submenu_rect = draw_menu(current_item, menu_rect.bx + menu_gap, 1)
submenu_is_hovered = get_point_to_rectangle_proximity(cursor, submenu_rect) == 0
if submenu_is_hovered then
cursor.on_primary_down = function() self:open_selected_item({preselect_first_item = false}) end
end
end
for index = start_index, end_index, 1 do
local item = menu.items[index]
if not item then break end
local item_ay = ay - menu.scroll_y + self.scroll_step * (index - 1)
local item_by = item_ay + self.item_height
local item_center_y = item_ay + (self.item_height / 2)
local item_clip = (item_ay < ay or item_by > by) and scroll_clip or nil
local content_ax, content_bx = ax + spacing, bx - spacing
local is_selected = menu.selected_index == index or item.active
-- Select hovered item
if is_current and self.mouse_nav then
if submenu_rect and cursor.direction_to_rectangle_distance(submenu_rect) then
blur_selected_index = false
else
local item_rect_hitbox = {
ax = menu_rect.ax + menu_padding,
ay = item_ay,
bx = menu_rect.bx + (item.items and menu_gap or -menu_padding), -- to bridge the gap with cursor
by = item_by
}
if submenu_is_hovered or get_point_to_rectangle_proximity(cursor, item_rect_hitbox) == 0 then
blur_selected_index = false
menu.selected_index = index
end
end
end
local next_item = menu.items[index + 1]
local next_is_active = next_item and next_item.active
local next_is_highlighted = menu.selected_index == index + 1 or next_is_active
local font_color = item.active and fgt or bgt
local shadow_color = item.active and fg or bg
-- Separator
local separator_ay = item.separator and item_by - 1 or item_by
local separator_by = item_by + (item.separator and 2 or 1)
if is_selected then separator_ay = item_by + 1 end
if next_is_highlighted then separator_by = item_by end
if separator_by - separator_ay > 0 and item_by < by then
ass:rect(ax + spacing / 2, separator_ay, bx - spacing / 2, separator_by, {
color = fg, opacity = menu_opacity * (item.separator and 0.08 or 0.06),
})
end
-- Highlight
local highlight_opacity = 0 + (item.active and 0.8 or 0) + (menu.selected_index == index and 0.15 or 0)
if not is_submenu and highlight_opacity > 0 then
ass:rect(ax + menu_padding, item_ay, bx - menu_padding, item_by, {
radius = 2, color = fg, opacity = highlight_opacity * text_opacity,
clip = item_clip,
})
end
-- Icon
if item.icon then
local x, y = content_bx - (icon_size / 2), item_center_y
if item.icon == 'spinner' then
ass:spinner(x, y, icon_size * 1.5, {color = font_color, opacity = text_opacity * 0.8})
else
ass:icon(x, y, icon_size * 1.5, item.icon, {
color = font_color, opacity = text_opacity, clip = item_clip,
shadow = 1, shadow_color = shadow_color,
})
end
content_bx = content_bx - icon_size - spacing
end
local title_cut_x = content_bx
if item.hint_width > 0 then
-- controls title & hint clipping proportional to the ratio of their widths
local title_content_ratio = item.title_width / (item.title_width + item.hint_width)
title_cut_x = round(content_ax + (content_bx - content_ax - spacing) * title_content_ratio
+ (item.title_width > 0 and spacing / 2 or 0))
end
-- Hint
if item.hint then
item.ass_safe_hint = item.ass_safe_hint or ass_escape(item.hint)
local clip = '\\clip(' .. title_cut_x .. ',' ..
math.max(item_ay, ay) .. ',' .. bx .. ',' .. math.min(item_by, by) .. ')'
ass:txt(content_bx, item_center_y, 6, item.ass_safe_hint, {
size = self.font_size_hint, color = font_color, wrap = 2, opacity = 0.5 * menu_opacity, clip = clip,
shadow = 1, shadow_color = shadow_color,
})
end
-- Title
if item.title then
item.ass_safe_title = item.ass_safe_title or ass_escape(item.title)
local clip = '\\clip(' .. ax .. ',' .. math.max(item_ay, ay) .. ','
.. title_cut_x .. ',' .. math.min(item_by, by) .. ')'
local title_x, align = content_ax, 4
if item.align == 'right' then
title_x, align = title_cut_x, 6
elseif item.align == 'center' then
title_x, align = content_ax + (title_cut_x - content_ax) / 2, 5
end
ass:txt(title_x, item_center_y, align, item.ass_safe_title, {
size = self.font_size, color = font_color, italic = item.italic, bold = item.bold, wrap = 2,
opacity = text_opacity * (item.muted and 0.5 or 1), clip = clip,
shadow = 1, shadow_color = shadow_color,
})
end
end
-- Menu title
if draw_title then
local title_ay = ay - self.item_height
local title_height = self.item_height - 3
menu.ass_safe_title = menu.ass_safe_title or ass_escape(menu.title)
-- Background
ass:rect(ax + 2, title_ay, bx - 2, title_ay + title_height, {
color = fg, opacity = menu_opacity * 0.8, radius = 2,
})
ass:texture(ax + 2, title_ay, bx - 2, title_ay + title_height, 'n', {
size = 80, color = bg, opacity = menu_opacity * 0.1,
})
-- Title
ass:txt(ax + menu.width / 2, title_ay + (title_height / 2), 5, menu.ass_safe_title, {
size = self.font_size, bold = true, color = bg, wrap = 2, opacity = menu_opacity,
clip = '\\clip(' .. ax .. ',' .. title_ay .. ',' .. bx .. ',' .. ay .. ')',
})
end
-- Scrollbar
if menu.scroll_height > 0 then
local groove_height = menu.height - 2
local thumb_height = math.max((menu.height / (menu.scroll_height + menu.height)) * groove_height, 40)
local thumb_y = ay + 1 + ((menu.scroll_y / menu.scroll_height) * (groove_height - thumb_height))
ass:rect(bx - 3, thumb_y, bx - 1, thumb_y + thumb_height, {color = fg, opacity = menu_opacity * 0.8})
end
-- We are in mouse nav and cursor isn't hovering any item
if blur_selected_index then
menu.selected_index = nil
end
return menu_rect
end
-- Main menu
draw_menu(self.current, self.ax, 0)
-- Parent menus
local parent_menu = self.current.parent_menu
local parent_offset_x, parent_horizontal_index = self.ax, -1
while parent_menu do
parent_offset_x = parent_offset_x - parent_menu.width - menu_gap
draw_menu(parent_menu, parent_offset_x, parent_horizontal_index)
parent_horizontal_index = parent_horizontal_index - 1
parent_menu = parent_menu.parent_menu
end
return ass
end
return Menu

View File

@@ -1,259 +0,0 @@
local Element = require('elements/Element')
---@alias TopBarButtonProps {icon: string; background: string; anchor_id?: string; command: string|fun()}
---@class TopBarButton : Element
local TopBarButton = class(Element)
---@param id string
---@param props TopBarButtonProps
function TopBarButton:new(id, props) return Class.new(self, id, props) --[[@as TopBarButton]] end
function TopBarButton:init(id, props)
Element.init(self, id, props)
self.anchor_id = 'top_bar'
self.icon = props.icon
self.background = props.background
self.command = props.command
end
function TopBarButton:handle_cursor_down()
mp.command(type(self.command) == 'function' and self.command() or self.command)
end
function TopBarButton:render()
local visibility = self:get_visibility()
if visibility <= 0 then return end
local ass = assdraw.ass_new()
-- Background on hover
if self.proximity_raw == 0 then
ass:rect(self.ax, self.ay, self.bx, self.by, {color = self.background, opacity = visibility})
cursor.on_primary_down = function() self:handle_cursor_down() end
end
local width, height = self.bx - self.ax, self.by - self.ay
local icon_size = math.min(width, height) * 0.5
ass:icon(self.ax + width / 2, self.ay + height / 2, icon_size, self.icon, {
opacity = visibility, border = options.text_border,
})
return ass
end
--[[ TopBar ]]
---@class TopBar : Element
local TopBar = class(Element)
function TopBar:new() return Class.new(self) --[[@as TopBar]] end
function TopBar:init()
Element.init(self, 'top_bar')
self.size = 0
self.icon_size, self.spacing, self.font_size, self.title_bx, self.title_by = 1, 1, 1, 1, 1
self.show_alt_title = false
self.main_title, self.alt_title = nil, nil
local function get_maximized_command()
return state.border
and (state.fullscreen and 'set fullscreen no;cycle window-maximized' or 'cycle window-maximized')
or 'set window-maximized no;cycle fullscreen'
end
-- Order aligns from right to left
self.buttons = {
TopBarButton:new('tb_close', {icon = 'close', background = '2311e8', command = 'quit'}),
TopBarButton:new('tb_max', {icon = 'crop_square', background = '222222', command = get_maximized_command}),
TopBarButton:new('tb_min', {icon = 'minimize', background = '222222', command = 'cycle window-minimized'}),
}
self:decide_titles()
end
function TopBar:decide_enabled()
if options.top_bar == 'no-border' then
self.enabled = not state.border or state.fullscreen
else
self.enabled = options.top_bar == 'always'
end
self.enabled = self.enabled and (options.top_bar_controls or options.top_bar_title)
for _, element in ipairs(self.buttons) do
element.enabled = self.enabled and options.top_bar_controls
end
end
function TopBar:decide_titles()
self.alt_title = state.alt_title ~= '' and state.alt_title or nil
self.main_title = state.title ~= '' and state.title or nil
if (self.main_title == 'No file') then
self.main_title = t('No file')
end
-- Fall back to alt title if main is empty
if not self.main_title then
self.main_title, self.alt_title = self.alt_title, nil
end
-- Deduplicate the main and alt titles by checking if one completely
-- contains the other, and using only the longer one.
if self.main_title and self.alt_title and not self.show_alt_title then
local longer_title, shorter_title
if #self.main_title < #self.alt_title then
longer_title, shorter_title = self.alt_title, self.main_title
else
longer_title, shorter_title = self.main_title, self.alt_title
end
local escaped_shorter_title = string.gsub(shorter_title --[[@as string]], "[%(%)%.%+%-%*%?%[%]%^%$%%]", "%%%1")
if string.match(longer_title --[[@as string]], escaped_shorter_title) then
self.main_title, self.alt_title = longer_title, nil
end
end
end
function TopBar:update_dimensions()
self.size = state.fullormaxed and options.top_bar_size_fullscreen or options.top_bar_size
self.icon_size = round(self.size * 0.5)
self.spacing = math.ceil(self.size * 0.25)
self.font_size = math.floor((self.size - (self.spacing * 2)) * options.font_scale)
self.button_width = round(self.size * 1.15)
self.ay = Elements.window_border.size
self.bx = display.width - Elements.window_border.size
self.by = self.size + Elements.window_border.size
self.title_bx = self.bx - (options.top_bar_controls and (self.button_width * 3) or 0)
self.ax = options.top_bar_title and Elements.window_border.size or self.title_bx
local button_bx = self.bx
for _, element in pairs(self.buttons) do
element.ax, element.bx = button_bx - self.button_width, button_bx
element.ay, element.by = self.ay, self.by
button_bx = button_bx - self.button_width
end
end
function TopBar:toggle_title()
if options.top_bar_alt_title_place ~= 'toggle' then return end
self.show_alt_title = not self.show_alt_title
end
function TopBar:on_prop_title() self:decide_titles() end
function TopBar:on_prop_alt_title() self:decide_titles() end
function TopBar:on_prop_border()
self:decide_enabled()
self:update_dimensions()
end
function TopBar:on_prop_fullscreen()
self:decide_enabled()
self:update_dimensions()
end
function TopBar:on_prop_maximized()
self:decide_enabled()
self:update_dimensions()
end
function TopBar:on_display() self:update_dimensions() end
function TopBar:render()
local visibility = self:get_visibility()
if visibility <= 0 then return end
local ass = assdraw.ass_new()
-- Window title
if options.top_bar_title and (state.title or state.has_playlist) then
local bg_margin = math.floor((self.size - self.font_size) / 4)
local padding = self.font_size / 2
local title_ax = self.ax + bg_margin
local title_ay = self.ay + bg_margin
local max_bx = self.title_bx - self.spacing
-- Playlist position
if state.has_playlist then
local text = state.playlist_pos .. '' .. state.playlist_count
local formatted_text = '{\\b1}' .. state.playlist_pos .. '{\\b0\\fs' .. self.font_size * 0.9 .. '}/'
.. state.playlist_count
local opts = {size = self.font_size, wrap = 2, color = fgt, opacity = visibility}
local bx = round(title_ax + text_width(text, opts) + padding * 2)
ass:rect(title_ax, title_ay, bx, self.by - bg_margin, {color = fg, opacity = visibility, radius = 2})
ass:txt(title_ax + (bx - title_ax) / 2, self.ay + (self.size / 2), 5, formatted_text, opts)
title_ax = bx + bg_margin
local rect = {ax = self.ax, ay = self.ay, bx = bx, by = self.by}
if get_point_to_rectangle_proximity(cursor, rect) == 0 then
cursor.on_primary_down = function() mp.command('script-binding uosc/playlist') end
end
end
-- Skip rendering titles if there's not enough horizontal space
if max_bx - title_ax > self.font_size * 3 then
-- Main title
local main_title = self.show_alt_title and self.alt_title or self.main_title
if main_title then
local opts = {
size = self.font_size, wrap = 2, color = bgt, border = 1, border_color = bg, opacity = visibility,
clip = string.format('\\clip(%d, %d, %d, %d)', self.ax, self.ay, max_bx, self.by),
}
local bx = math.min(max_bx, title_ax + text_width(main_title, opts) + padding * 2)
local by = self.by - bg_margin
local title_rect = {ax = title_ax, ay = title_ay, bx = bx, by = by}
if options.top_bar_alt_title_place == 'toggle'
and get_point_to_rectangle_proximity(cursor, title_rect) == 0 then
cursor.on_primary_down = function() self:toggle_title() end
cursor.allow_dragging = true
end
ass:rect(title_rect.ax, title_rect.ay, title_rect.bx, title_rect.by, {
color = bg, opacity = visibility * options.top_bar_title_opacity, radius = 2,
})
ass:txt(title_ax + padding, self.ay + (self.size / 2), 4, main_title, opts)
title_ay = by + 1
end
-- Alt title
if self.alt_title and options.top_bar_alt_title_place == 'below' then
local font_size = self.font_size * 0.9
local height = font_size * 1.3
local by = title_ay + height
local opts = {
size = font_size, wrap = 2, color = bgt, border = 1, border_color = bg, opacity = visibility
}
local bx = math.min(max_bx, title_ax + text_width(self.alt_title, opts) + padding * 2)
opts.clip = string.format('\\clip(%d, %d, %d, %d)', title_ax, title_ay, bx, by)
ass:rect(title_ax, title_ay, bx, by, {
color = bg, opacity = visibility * options.top_bar_title_opacity, radius = 2,
})
ass:txt(title_ax + padding, title_ay + height / 2, 4, self.alt_title, opts)
title_ay = by + 1
end
-- Subtitle: current chapter
if state.current_chapter then
local font_size = self.font_size * 0.8
local height = font_size * 1.3
local text = '' .. state.current_chapter.index .. ': ' .. state.current_chapter.title
local by = title_ay + height
local opts = {
size = font_size, italic = true, wrap = 2, color = bgt,
border = 1, border_color = bg, opacity = visibility * 0.8,
}
local bx = math.min(max_bx, title_ax + text_width(text, opts) + padding * 2)
opts.clip = string.format('\\clip(%d, %d, %d, %d)', title_ax, title_ay, bx, by)
ass:rect(title_ax, title_ay, bx, by, {
color = bg, opacity = visibility * options.top_bar_title_opacity, radius = 2,
})
ass:txt(title_ax + padding, title_ay + height / 2, 4, text, opts)
title_ay = by + 1
end
end
self.title_by = title_ay - 1
else
self.title_by = self.ay
end
return ass
end
return TopBar

View File

@@ -1,59 +0,0 @@
{
"Aspect ratio": "Seitenverhältnis",
"Audio": "Audio",
"Audio device": "Audiogerät",
"Audio devices": "Audiogeräte",
"Audio tracks": "Audiospuren",
"Autoselect device": "Automatische Geräteauswahl",
"Chapter %s": "Kapitel %s",
"Chapters": "Kapitel",
"Default": "Standard",
"Default %s": "Standard %s",
"Delete file & Next": "Lösche Datei & Nächstes",
"Delete file & Prev": "Lösche Datei & Vorheriges",
"Delete file & Quit": "Lösche Datei & Beenden",
"Disabled": "Deaktiviert",
"Drives": "Laufwerke",
"Edition": "Edition",
"Edition %s": "Edition %s",
"Editions": "Editionen",
"Empty": "Leer",
"First": "Erstes",
"Fullscreen": "Vollbild",
"Last": "Letztes",
"Load": "Hinzufügen",
"Load audio": "Audio hinzufügen",
"Load subtitles": "Untertitel hinzufügen",
"Load video": "Video hinzufügen",
"Loop file": "Datei wiederholen",
"Loop playlist": "Wiedergabeliste wiederholen",
"Menu": "Menü",
"Navigation": "Navigation",
"Next": "Nächstes",
"No file": "Keine Datei",
"Open config folder": "Konfigurationsordner öffnen",
"Open file": "Datei öffnen",
"Playlist": "Wiedergabeliste",
"Playlist/Files": "Wiedergabeliste/Dateien",
"Prev": "Vorheriges",
"Previous": "Vorheriges",
"Quit": "Beenden",
"Screenshot": "Bildschirmfoto",
"Show in directory": "Im Verzeichnis anzeigen",
"Shuffle": "Zufällig",
"Stream quality": "Streamqualität",
"Subtitles": "Untertitel",
"Track": "Spur",
"Track %s": "Spur %s",
"Utils": "Werkzeuge",
"Video": "Video",
"%s channels": "%s Kanäle",
"%s channel": "%s Kanal",
"default": "Standard",
"drive": "Laufwerk",
"external": "extern",
"forced": "erzwungen",
"open file": "Datei öffnen",
"parent dir": "übergeordnetes Verzeichnis",
"playlist or file": "Wiedergabeliste oder Datei"
}

View File

@@ -1,59 +0,0 @@
{
"Aspect ratio": "Relación de aspecto",
"Audio": "Audio",
"Audio device": "Dispositivo de audio",
"Audio devices": "Dispositivos de audio",
"Audio tracks": "Pistas de audio",
"Autoselect device": "Selección automática",
"Chapter %s": "Capítulo %s",
"Chapters": "Capítulos",
"Default": "Por defecto",
"Default %s": "Por defecto %s",
"Delete file & Next": "Eliminar archivo y siguiente",
"Delete file & Prev": "Eliminar archivo y anterior",
"Delete file & Quit": "Eliminar archivo y salir",
"Disabled": "Desactivado",
"Drives": "Unidades",
"Edition": "Edición",
"Edition %s": "Edición %s",
"Editions": "Ediciones",
"Empty": "Vacío",
"First": "Primero",
"Fullscreen": "Pantalla completa",
"Last": "Último",
"Load": "Abrir",
"Load audio": "Añadir una pista de audio",
"Load subtitles": "Añadir una pista de subtítulos",
"Load video": "Añadir una pista de vídeo",
"Loop file": "Repetir archivo",
"Loop playlist": "Repetir lista de reproducción",
"Menu": "Menú",
"Navigation": "Navegación",
"Next": "Siguiente",
"No file": "Ningún archivo",
"Open config folder": "Abrir carpeta de configuración",
"Open file": "Abrir un archivo",
"Playlist": "Lista de reproducción",
"Playlist/Files": "Lista de reproducción / Archivos",
"Prev": "Anterior",
"Previous": "Anterior",
"Quit": "Salir",
"Screenshot": "Captura de pantalla",
"Show in directory": "Acceder a la carpeta",
"Shuffle": "Reproducción aleatoria",
"Stream quality": "Calidad del flujo",
"Subtitles": "Subtítulos",
"Track": "Pista",
"Track %s": "Pista %s",
"Utils": "Utilidades",
"Video": "Vídeo",
"%s channel": "%s canal",
"%s channels": "%s canales",
"default": "por defecto",
"drive": "unidad",
"external": "externo",
"forced": "forzado",
"open file": "seleccionar un archivo",
"parent dir": "directorio padre",
"playlist or file": "archivo o lista de reproducción"
}

View File

@@ -1,59 +0,0 @@
{
"Aspect ratio": "Format d'image",
"Audio": "Audio",
"Audio device": "Périphérique audio",
"Audio devices": "Périphériques audio",
"Audio tracks": "Pistes audio",
"Autoselect device": "Sélection automatique",
"Chapter %s": "Chapitre %s",
"Chapters": "Chapitres",
"Default": "Par défaut",
"Default %s": "Par défaut %s",
"Delete file & Next": "Supprimer le fichier et Suivant",
"Delete file & Prev": "Supprimer le fichier et Précédent",
"Delete file & Quit": "Supprimer le fichier et Quitter",
"Disabled": "Désactivé",
"Drives": "Lecteurs",
"Edition": "Édition",
"Edition %s": "Édition %s",
"Editions": "Éditions",
"Empty": "Vide",
"First": "Premier",
"Fullscreen": "Plein écran",
"Last": "Dernier",
"Load": "Ouvrir",
"Load audio": "Ajouter une piste audio",
"Load subtitles": "Ajouter une piste de sous-titres",
"Load video": "Ajouter une piste vidéo",
"Loop file": "Lire en boucle le fichier",
"Loop playlist": "Lire en boucle la liste de lecture",
"Menu": "Menu",
"Navigation": "Navigation",
"Next": "Suivant",
"No file": "Aucun fichier",
"Open config folder": "Ouvrir le dossier de configuration",
"Open file": "Ouvrir un fichier",
"Playlist": "Liste de lecture",
"Playlist/Files": "Liste de lecture / Fichiers",
"Prev": "Précédent",
"Previous": "Précédent",
"Quit": "Quitter",
"Screenshot": "Capture d'écran",
"Show in directory": "Accéder au dossier",
"Shuffle": "Lecture aléatoire",
"Stream quality": "Qualité du flux",
"Subtitles": "Sous-titres",
"Track": "Piste",
"Track %s": "Piste %s",
"Utils": "Outils",
"Video": "Vidéo",
"%s channel": "%s canal",
"%s channels": "%s canaux",
"default": "par défaut",
"drive": "lecteur",
"external": "externe",
"forced": "forcé",
"open file": "sélectionner un fichier",
"parent dir": "répertoire parent",
"playlist or file": "fichier ou liste de lecture"
}

View File

@@ -1,59 +0,0 @@
{
"Aspect ratio": "Raportul de aspect",
"Audio": "Audio",
"Audio device": "Dispozitiv audio",
"Audio devices": "Dispozitive audio",
"Audio tracks": "Piese audio",
"Autoselect device": "Selectare automată",
"Chapter %s": "Capitolul %s",
"Chapters": "Capitole",
"Default": "Implicit",
"Default %s": "Implicit %s",
"Delete file & Next": "Ștergere fișier și următorul",
"Delete file & Prev": "Ștergere fișier și anteriorul",
"Delete file & Quit": "Ștergere fișier și ieși",
"Disabled": "Dezactivat",
"Drives": "Unități",
"Edition": "Ediție",
"Edition %s": "Ediție %s",
"Editions": "Ediții",
"Empty": "Gol",
"First": "Primul",
"Fullscreen": "Ecran complet",
"Last": "Ultimul",
"Load": "Încarcă",
"Load audio": "Deschide audio",
"Load subtitles": "Deschide subtitrările",
"Load video": "Deschide video",
"Loop file": "Repetă fișierul",
"Loop playlist": "Repetă lista de redare",
"Menu": "Meniu",
"Navigation": "Navigare",
"Next": "Următor",
"No file": "Niciun fisier",
"Open config folder": "Deschide dosarul de configurație",
"Open file": "Deschide fișierul",
"Playlist": "Listă de redare",
"Playlist/Files": "Listă de redare/Fișiere",
"Prev": "Precedent",
"Previous": "Precedent",
"Quit": "Ieșire",
"Screenshot": "Captură de ecran",
"Show in directory": "Arată în dosar",
"Shuffle": "Amestecă",
"Stream quality": "Calitatea fluxului",
"Subtitles": "Subtitrări",
"Track": "Pistă",
"Track %s": "Pistă %s",
"Utils": "Utilități",
"Video": "Video",
"%s channel": "%s canal",
"%s channels": "%s canale",
"default": "implicit",
"drive": "unitate",
"external": "extern",
"forced": "forțat",
"open file": "deschide fișierul",
"parent dir": "director părinte",
"playlist or file": "fișier sau listă de redare"
}

View File

@@ -1,59 +0,0 @@
{
"Aspect ratio": "纵横比",
"Audio": "音频",
"Audio device": "音频设备",
"Audio devices": "音频设备",
"Audio tracks": "音频轨道",
"Autoselect device": "自动选择",
"Chapter %s": "第 %s 章",
"Chapters": "章节",
"Default": "默认",
"Default %s": "默认 %s",
"Delete file & Next": "删除文件并播放下一个",
"Delete file & Prev": "删除文件并播放上一个",
"Delete file & Quit": "删除文件并退出",
"Disabled": "禁用",
"Drives": "驱动器",
"Edition": "版本",
"Edition %s": "版本 %s",
"Editions": "版本",
"Empty": "空",
"First": "第一个",
"Fullscreen": "全屏",
"Last": "最后一个",
"Load": "加载",
"Load audio": "加载音频",
"Load subtitles": "加载字幕",
"Load video": "加载视频",
"Loop file": "单个循环",
"Loop playlist": "列表循环",
"Menu": "菜单",
"Navigation": "导航",
"Next": "下一个",
"No file": "无文件",
"Open config folder": "打开设置文件夹",
"Open file": "打开文件",
"Playlist": "播放列表",
"Playlist/Files": "播放/文件列表",
"Prev": "上一个",
"Previous": "上一个",
"Quit": "退出",
"Screenshot": "截图",
"Show in directory": "打开所在文件夹",
"Shuffle": "乱序",
"Stream quality": "流媒体品质",
"Subtitles": "字幕",
"Track": "轨道",
"Track %s": "轨道 %s",
"Utils": "工具",
"Video": "视频",
"%s channel": "%s 声道",
"%s channels": "%s 声道",
"default": "默认",
"drive": "磁盘",
"external": "外置",
"forced": "强制",
"open file": "打开文件",
"parent dir": "父文件夹",
"playlist or file": "播放列表或文件"
}

View File

@@ -1,170 +0,0 @@
--[[ ASSDRAW EXTENSIONS ]]
local ass_mt = getmetatable(assdraw.ass_new())
-- Opacity.
---@param opacity number|number[] Opacity of all elements, or an array of [primary, secondary, border, shadow] opacities.
---@param fraction? number Optionally adjust the above opacity by this fraction.
function ass_mt:opacity(opacity, fraction)
fraction = fraction ~= nil and fraction or 1
if type(opacity) == 'number' then
self.text = self.text .. string.format('{\\alpha&H%X&}', opacity_to_alpha(opacity * fraction))
else
self.text = self.text .. string.format(
'{\\1a&H%X&\\2a&H%X&\\3a&H%X&\\4a&H%X&}',
opacity_to_alpha((opacity[1] or 0) * fraction),
opacity_to_alpha((opacity[2] or 0) * fraction),
opacity_to_alpha((opacity[3] or 0) * fraction),
opacity_to_alpha((opacity[4] or 0) * fraction)
)
end
end
-- Icon.
---@param x number
---@param y number
---@param size number
---@param name string
---@param opts? {color?: string; border?: number; border_color?: string; opacity?: number; clip?: string; align?: number}
function ass_mt:icon(x, y, size, name, opts)
opts = opts or {}
opts.font, opts.size, opts.bold = 'MaterialIconsRound-Regular', size, false
self:txt(x, y, opts.align or 5, name, opts)
end
-- Text.
-- Named `txt` because `ass.text` is a value.
---@param x number
---@param y number
---@param align number
---@param value string|number
---@param opts {size: number; font?: string; color?: string; bold?: boolean; italic?: boolean; border?: number; border_color?: string; shadow?: number; shadow_color?: string; rotate?: number; wrap?: number; opacity?: number; clip?: string}
function ass_mt:txt(x, y, align, value, opts)
local border_size = opts.border or 0
local shadow_size = opts.shadow or 0
local tags = '\\pos(' .. x .. ',' .. y .. ')\\rDefault\\an' .. align .. '\\blur0'
-- font
tags = tags .. '\\fn' .. (opts.font or config.font)
-- font size
tags = tags .. '\\fs' .. opts.size
-- bold
if opts.bold or (opts.bold == nil and options.font_bold) then tags = tags .. '\\b1' end
-- italic
if opts.italic then tags = tags .. '\\i1' end
-- rotate
if opts.rotate then tags = tags .. '\\frz' .. opts.rotate end
-- wrap
if opts.wrap then tags = tags .. '\\q' .. opts.wrap end
-- border
tags = tags .. '\\bord' .. border_size
-- shadow
tags = tags .. '\\shad' .. shadow_size
-- colors
tags = tags .. '\\1c&H' .. (opts.color or bgt)
if border_size > 0 then tags = tags .. '\\3c&H' .. (opts.border_color or bg) end
if shadow_size > 0 then tags = tags .. '\\4c&H' .. (opts.shadow_color or bg) end
-- opacity
if opts.opacity then tags = tags .. string.format('\\alpha&H%X&', opacity_to_alpha(opts.opacity)) end
-- clip
if opts.clip then tags = tags .. opts.clip end
-- render
self:new_event()
self.text = self.text .. '{' .. tags .. '}' .. value
end
-- Tooltip.
---@param element {ax: number; ay: number; bx: number; by: number}
---@param value string|number
---@param opts? {size?: number; offset?: number; bold?: boolean; italic?: boolean; width_overwrite?: number, responsive?: boolean}
function ass_mt:tooltip(element, value, opts)
opts = opts or {}
opts.size = opts.size or 16
opts.border = options.text_border
opts.border_color = bg
local offset = opts.offset or opts.size / 2
local align_top = opts.responsive == false or element.ay - offset > opts.size * 2
local x = element.ax + (element.bx - element.ax) / 2
local y = align_top and element.ay - offset or element.by + offset
local margin = (opts.width_overwrite or text_width(value, opts)) / 2 + 10 + Elements.window_border.size
self:txt(clamp(margin, x, display.width - margin), y, align_top and 2 or 8, value, opts)
end
-- Rectangle.
---@param ax number
---@param ay number
---@param bx number
---@param by number
---@param opts? {color?: string; border?: number; border_color?: string; opacity?: number; border_opacity?: number; clip?: string, radius?: number}
function ass_mt:rect(ax, ay, bx, by, opts)
opts = opts or {}
local border_size = opts.border or 0
local tags = '\\pos(0,0)\\rDefault\\an7\\blur0'
-- border
tags = tags .. '\\bord' .. border_size
-- colors
tags = tags .. '\\1c&H' .. (opts.color or fg)
if border_size > 0 then tags = tags .. '\\3c&H' .. (opts.border_color or bg) end
-- opacity
if opts.opacity then tags = tags .. string.format('\\alpha&H%X&', opacity_to_alpha(opts.opacity)) end
if opts.border_opacity then tags = tags .. string.format('\\3a&H%X&', opacity_to_alpha(opts.border_opacity)) end
-- clip
if opts.clip then
tags = tags .. opts.clip
end
-- draw
self:new_event()
self.text = self.text .. '{' .. tags .. '}'
self:draw_start()
if opts.radius then
self:round_rect_cw(ax, ay, bx, by, opts.radius)
else
self:rect_cw(ax, ay, bx, by)
end
self:draw_stop()
end
-- Circle.
---@param x number
---@param y number
---@param radius number
---@param opts? {color?: string; border?: number; border_color?: string; opacity?: number; clip?: string}
function ass_mt:circle(x, y, radius, opts)
opts = opts or {}
opts.radius = radius
self:rect(x - radius, y - radius, x + radius, y + radius, opts)
end
-- Texture.
---@param ax number
---@param ay number
---@param bx number
---@param by number
---@param char string Texture font character.
---@param opts {size?: number; color: string; opacity?: number; clip?: string; anchor_x?: number, anchor_y?: number}
function ass_mt:texture(ax, ay, bx, by, char, opts)
opts = opts or {}
local anchor_x, anchor_y = opts.anchor_x or ax, opts.anchor_y or ay
local clip = opts.clip or ('\\clip(' .. ax .. ',' .. ay .. ',' .. bx .. ',' .. by .. ')')
local tile_size, opacity = opts.size or 100, opts.opacity or 0.2
local x, y = ax - (ax - anchor_x) % tile_size, ay - (ay - anchor_y) % tile_size
local width, height = bx - x, by - y
local line = string.rep(char, math.ceil((width / tile_size)))
local lines = ''
for i = 1, math.ceil(height / tile_size), 1 do lines = lines .. (lines == '' and '' or '\\N') .. line end
self:txt(
x, y, 7, lines,
{font = 'uosc_textures', size = tile_size, color = opts.color, bold = false, opacity = opacity, clip = clip})
end
-- Rotating spinner icon.
---@param x number
---@param y number
---@param size number
---@param opts? {color?: string; opacity?: number; clip?: string; border?: number; border_color?: string;}
function ass_mt:spinner(x, y, size, opts)
opts = opts or {}
opts.rotate = (state.render_last_time * 1.75 % 1) * -360
opts.color = opts.color or fg
self:icon(x, y, size, 'autorenew', opts)
request_render()
end

View File

@@ -1,292 +0,0 @@
---@param data MenuData
---@param opts? {submenu?: string; mouse_nav?: boolean; on_close?: string | string[]}
function open_command_menu(data, opts)
local function run_command(command)
if type(command) == 'string' then
mp.command(command)
else
---@diagnostic disable-next-line: deprecated
mp.commandv(unpack(command))
end
end
---@type MenuOptions
local menu_opts = {}
if opts then
menu_opts.mouse_nav = opts.mouse_nav
if opts.on_close then menu_opts.on_close = function() run_command(opts.on_close) end end
end
local menu = Menu:open(data, run_command, menu_opts)
if opts and opts.submenu then menu:activate_submenu(opts.submenu) end
return menu
end
---@param opts? {submenu?: string; mouse_nav?: boolean; on_close?: string | string[]}
function toggle_menu_with_items(opts)
if Menu:is_open('menu') then Menu:close()
else open_command_menu({type = 'menu', items = config.menu_items}, opts) end
end
---@param options {type: string; title: string; list_prop: string; active_prop?: string; serializer: fun(list: any, active: any): MenuDataItem[]; on_select: fun(value: any); on_move_item?: fun(from_index: integer, to_index: integer, submenu_path: integer[]); on_delete_item?: fun(index: integer, submenu_path: integer[])}
function create_self_updating_menu_opener(options)
return function()
if Menu:is_open(options.type) then Menu:close() return end
local list = mp.get_property_native(options.list_prop)
local active = options.active_prop and mp.get_property_native(options.active_prop) or nil
local menu
local function update() menu:update_items(options.serializer(list, active)) end
local ignore_initial_list = true
local function handle_list_prop_change(name, value)
if ignore_initial_list then ignore_initial_list = false
else list = value update() end
end
local ignore_initial_active = true
local function handle_active_prop_change(name, value)
if ignore_initial_active then ignore_initial_active = false
else active = value update() end
end
local initial_items, selected_index = options.serializer(list, active)
-- Items and active_index are set in the handle_prop_change callback, since adding
-- a property observer triggers its handler immediately, we just let that initialize the items.
menu = Menu:open(
{type = options.type, title = options.title, items = initial_items, selected_index = selected_index},
options.on_select, {
on_open = function()
mp.observe_property(options.list_prop, 'native', handle_list_prop_change)
if options.active_prop then
mp.observe_property(options.active_prop, 'native', handle_active_prop_change)
end
end,
on_close = function()
mp.unobserve_property(handle_list_prop_change)
mp.unobserve_property(handle_active_prop_change)
end,
on_move_item = options.on_move_item,
on_delete_item = options.on_delete_item,
})
end
end
function create_select_tracklist_type_menu_opener(menu_title, track_type, track_prop, load_command)
local function serialize_tracklist(tracklist)
local items = {}
if load_command then
items[#items + 1] = {
title = t('Load'), bold = true, italic = true, hint = t('open file'), value = '{load}', separator = true,
}
end
local first_item_index = #items + 1
local active_index = nil
local disabled_item = nil
-- Add option to disable a subtitle track. This works for all tracks,
-- but why would anyone want to disable audio or video? Better to not
-- let people mistakenly select what is unwanted 99.999% of the time.
-- If I'm mistaken and there is an active need for this, feel free to
-- open an issue.
if track_type == 'sub' then
disabled_item = {title = t('Disabled'), italic = true, muted = true, hint = '', value = nil, active = true}
items[#items + 1] = disabled_item
end
for _, track in ipairs(tracklist) do
if track.type == track_type then
local hint_values = {}
local function h(value) hint_values[#hint_values + 1] = value end
if track.lang then h(track.lang:upper()) end
if track['demux-h'] then
h(track['demux-w'] and (track['demux-w'] .. 'x' .. track['demux-h']) or (track['demux-h'] .. 'p'))
end
if track['demux-fps'] then h(string.format('%.5gfps', track['demux-fps'])) end
h(track.codec)
if track['audio-channels'] then h(t(track['audio-channels'] == 1 and '%s channel' or '%s channels', track['audio-channels'])) end
if track['demux-samplerate'] then h(string.format('%.3gkHz', track['demux-samplerate'] / 1000)) end
if track.forced then h(t('forced')) end
if track.default then h(t('default')) end
if track.external then h(t('external')) end
items[#items + 1] = {
title = (track.title and track.title or t('Track %s', track.id)),
hint = table.concat(hint_values, ', '),
value = track.id,
active = track.selected,
}
if track.selected then
if disabled_item then disabled_item.active = false end
active_index = #items
end
end
end
return items, active_index or first_item_index
end
local function selection_handler(value)
if value == '{load}' then
mp.command(load_command)
else
mp.commandv('set', track_prop, value and value or 'no')
-- If subtitle track was selected, assume user also wants to see it
if value and track_type == 'sub' then
mp.commandv('set', 'sub-visibility', 'yes')
end
end
end
return create_self_updating_menu_opener({
title = menu_title,
type = track_type,
list_prop = 'track-list',
serializer = serialize_tracklist,
on_select = selection_handler,
})
end
---@alias NavigationMenuOptions {type: string, title?: string, allowed_types?: string[], active_path?: string, selected_path?: string; on_open?: fun(); on_close?: fun()}
-- Opens a file navigation menu with items inside `directory_path`.
---@param directory_path string
---@param handle_select fun(path: string): nil
---@param opts NavigationMenuOptions
function open_file_navigation_menu(directory_path, handle_select, opts)
directory = serialize_path(normalize_path(directory_path))
opts = opts or {}
if not directory then
msg.error('Couldn\'t serialize path "' .. directory_path .. '.')
return
end
local files, directories = read_directory(directory.path, opts.allowed_types)
local is_root = not directory.dirname
local path_separator = path_separator(directory.path)
if not files or not directories then return end
sort_filenames(directories)
sort_filenames(files)
-- Pre-populate items with parent directory selector if not at root
-- Each item value is a serialized path table it points to.
local items = {}
if is_root then
if state.platform == 'windows' then
items[#items + 1] = {title = '..', hint = t('Drives'), value = '{drives}', separator = true}
end
else
items[#items + 1] = {title = '..', hint = t('parent dir'), value = directory.dirname, separator = true}
end
local back_path = items[#items] and items[#items].value
local selected_index = #items + 1
for _, dir in ipairs(directories) do
items[#items + 1] = {title = dir, value = join_path(directory.path, dir), hint = path_separator}
end
for _, file in ipairs(files) do
items[#items + 1] = {title = file, value = join_path(directory.path, file)}
end
for index, item in ipairs(items) do
if not item.value.is_to_parent and opts.active_path == item.value then
item.active = true
if not opts.selected_path then selected_index = index end
end
if opts.selected_path == item.value then selected_index = index end
end
---@type MenuCallback
local function open_path(path, meta)
local is_drives = path == '{drives}'
local is_to_parent = is_drives or #path < #directory_path
local inheritable_options = {
type = opts.type, title = opts.title, allowed_types = opts.allowed_types, active_path = opts.active_path,
}
if is_drives then
open_drives_menu(function(drive_path)
open_file_navigation_menu(drive_path, handle_select, inheritable_options)
end, {
type = inheritable_options.type, title = inheritable_options.title, selected_path = directory.path,
on_open = opts.on_open, on_close = opts.on_close,
})
return
end
local info, error = utils.file_info(path)
if not info then
msg.error('Can\'t retrieve path info for "' .. path .. '". Error: ' .. (error or ''))
return
end
if info.is_dir and not meta.modifiers.ctrl then
-- Preselect directory we are coming from
if is_to_parent then
inheritable_options.selected_path = directory.path
end
open_file_navigation_menu(path, handle_select, inheritable_options)
else
handle_select(path)
end
end
local function handle_back()
if back_path then open_path(back_path, {modifiers = {}}) end
end
local menu_data = {
type = opts.type, title = opts.title or directory.basename .. path_separator, items = items,
selected_index = selected_index,
}
local menu_options = {on_open = opts.on_open, on_close = opts.on_close, on_back = handle_back}
return Menu:open(menu_data, open_path, menu_options)
end
-- Opens a file navigation menu with Windows drives as items.
---@param handle_select fun(path: string): nil
---@param opts? NavigationMenuOptions
function open_drives_menu(handle_select, opts)
opts = opts or {}
local process = mp.command_native({
name = 'subprocess',
capture_stdout = true,
playback_only = false,
args = {'wmic', 'logicaldisk', 'get', 'name', '/value'},
})
local items, selected_index = {}, 1
if process.status == 0 then
for _, value in ipairs(split(process.stdout, '\n')) do
local drive = string.match(value, 'Name=([A-Z]:)')
if drive then
local drive_path = normalize_path(drive)
items[#items + 1] = {
title = drive, hint = t('drive'), value = drive_path, active = opts.active_path == drive_path,
}
if opts.selected_path == drive_path then selected_index = #items end
end
end
else
msg.error(process.stderr)
end
return Menu:open(
{type = opts.type, title = opts.title or t('Drives'), items = items, selected_index = selected_index},
handle_select
)
end

157
src/tools/lib/zip.go Normal file
View File

@@ -0,0 +1,157 @@
package lib
import (
"archive/zip"
"io"
"io/fs"
"os"
"path/filepath"
)
// CountingWriter wraps an io.Writer and counts the number of bytes written.
type CountingWriter struct {
written int64
writer io.Writer
}
// Write writes bytes and counts them.
func (cw *CountingWriter) Write(p []byte) (int, error) {
n, err := cw.writer.Write(p)
cw.written += int64(n)
return n, err
}
/*
`files` format:
```
map[string]string{
"/path/on/disk/file1.txt": "file1.txt",
"/path/on/disk/file2.txt": "subfolder/file2.txt",
"/path/on/disk/file3.txt": "", // put in root of archive as file3.txt
"/path/on/disk/file4.txt": "subfolder/", // put in subfolder as file4.txt
"/path/on/disk/folder": "Custom Folder", // contents added recursively
}
```
*/
func ZipFilesWithHeaders(files map[string]string, outputFile string, headerMod HeaderModFn) (ZipStats, error) {
path, err := filepath.Abs(outputFile)
if err != nil {
return ZipStats{}, err
}
dirname := filepath.Dir(path)
err = os.MkdirAll(dirname, os.ModePerm)
if err != nil {
return ZipStats{}, err
}
f, err := os.Create(path)
if err != nil {
return ZipStats{}, err
}
defer f.Close()
countedF := &CountingWriter{writer: f}
zw := zip.NewWriter(countedF)
defer zw.Close()
var filesNum, bytes int64
addFile := func(srcPath string, nameInArchive string, entry fs.DirEntry) error {
src, err := os.Open(srcPath)
if err != nil {
return err
}
defer src.Close()
info, err := entry.Info()
if err != nil {
return err
}
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
header.Name = nameInArchive
header.Method = zip.Deflate
header = headerMod(header)
if header.Name == "" {
return nil
}
dst, err := zw.CreateHeader(header)
if err != nil {
return err
}
written, err := io.Copy(dst, src)
if err != nil {
return err
}
bytes += written
filesNum++
return nil
}
for src, dst := range files {
stat, err := os.Stat(src)
if err != nil {
return ZipStats{}, err
}
basename := filepath.Base(src)
if dst == "" {
dst = basename
} else if dst[len(dst)-1:] == "/" {
dst = dst + basename
}
if !stat.IsDir() {
addFile(src, dst, fs.FileInfoToDirEntry(stat))
continue
}
err = filepath.WalkDir(src, func(path string, entry fs.DirEntry, err error) error {
if err != nil {
return err
}
if entry.IsDir() {
return nil
}
relativePath, err := filepath.Rel(src, path)
if err != nil {
return err
}
err = addFile(path, dst+"/"+filepath.ToSlash(relativePath), entry)
if err != nil {
return err
}
return nil
})
if err != nil {
return ZipStats{}, err
}
}
return ZipStats{
FilesNum: filesNum,
TotalBytes: bytes,
CompressedBytes: countedF.written,
}, nil
}
// If `HeaderModFn` function sets `header.Name` to empty string, file will be skipped.
type HeaderModFn func(header *zip.FileHeader) *zip.FileHeader
type ZipStats struct {
FilesNum int64
TotalBytes int64
CompressedBytes int64
}

39
src/tools/tools.go Normal file
View File

@@ -0,0 +1,39 @@
package main
import (
"fmt"
"os"
"uosc/bins/src/tools/tools"
)
func main() {
command := "help"
if len(os.Args) > 1 {
command = os.Args[1]
}
switch command {
case "intl":
tools.Intl(os.Args[2:])
case "package":
tools.Packager(os.Args[2:])
// Help
default:
fmt.Printf(`uosc tools.
Usage:
tools <command> [args]
Available <command>s:
intl - localization helper
package - package uosc release files
Run 'tools <command> -h/--help' for help on how to use each tool.
`)
}
}

12
src/tools/tools/base.go Normal file
View File

@@ -0,0 +1,12 @@
package tools
func check(err error) {
if err != nil {
panic(err)
}
}
func must[T any](t T, err error) T {
check(err)
return t
}

270
src/tools/tools/intl.go Normal file
View File

@@ -0,0 +1,270 @@
package tools
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"golang.org/x/exp/maps"
"k8s.io/apimachinery/pkg/util/sets"
)
func Intl(args []string) {
cwd, err := os.Getwd()
check(err)
uoscRootRelative := "src/uosc"
intlRootRelative := uoscRootRelative + "/intl"
uoscRoot := filepath.Join(cwd, uoscRootRelative)
// Check we're in correct location
if stat, err := os.Stat(uoscRoot); os.IsNotExist(err) || !stat.IsDir() {
fmt.Printf(`Directory "%s" doesn't exist. Make sure you're running this tool in uosc's project root folder as current working directory.`, uoscRootRelative)
os.Exit(1)
}
// Help
if len(args) < 1 || len(args) > 0 && sets.New("--help", "-h").Has(args[0]) {
fmt.Printf(`Updates or creates a localization files by parsing the codebase for localization strings, and (re)constructing the locale files with them.
Strings no longer in use are removed. Strings not yet translated are set to "null".
Usage:
intl [languages]
Parameters:
languages A comma separated list of language codes to update
or create. Use 'all' to update all existing locales.
Examples:
> intl xy
Create a new locale xy.
> intl de,es
Update de and es locales.
> intl all
Update everything inside "%s".
`, intlRootRelative)
os.Exit(0)
}
var locales []string
if args[0] == "all" {
intlRoot := filepath.Join(cwd, intlRootRelative)
locales = must(listFilenamesOfType(intlRoot, ".json"))
} else {
locales = strings.Split(args[0], ",")
}
holePunchLocales(locales, uoscRoot)
}
func holePunchLocales(locales []string, rootPath string) {
fmt.Println("Creating localization holes for:", strings.Join(locales, ", "))
fnName := 't'
spaces := sets.New(' ', '\t', '\n')
enclosers := sets.New('"', '\'')
wordBreaks := sets.New('=', '*', '+', '-', '/', '(', ')', '^', '%', '#', '@', '!', '~', '`', '"', '\'', ' ', '\t', '\n')
escape := '\\'
openParen := '('
localizationStrings := sets.New[string]()
// Contents processor to extract localization strings
// Solution doesn't check if function calls are commented out or not.
processFile := func(path string) {
escapesNum := 0
f := must(os.Open(path))
currentStr := ""
currentEncloser := '"'
prevRune := ' '
type lexFn func(r rune)
var currentLexer lexFn
var accumulateString lexFn
var findOpenEncloser lexFn
var findOpenParen lexFn
var findFn lexFn
commitStr := func() {
localizationStrings.Insert(currentStr)
currentStr = ""
currentLexer = findFn
}
accumulateString = func(r rune) {
if r == currentEncloser && escapesNum%2 == 0 {
commitStr()
} else {
if r == escape {
escapesNum++
} else {
escapesNum = 0
}
currentStr += string(r)
}
}
findOpenEncloser = func(r rune) {
if !spaces.Has(r) {
if enclosers.Has(r) {
currentEncloser = r
currentLexer = accumulateString
} else {
currentLexer = findFn
}
}
}
findOpenParen = func(r rune) {
if !spaces.Has(r) {
if r == openParen {
currentLexer = findOpenEncloser
} else {
currentLexer = findFn
}
}
}
findFn = func(b rune) {
if b == fnName && wordBreaks.Has(prevRune) {
currentLexer = findOpenParen
}
}
currentLexer = findFn
br := bufio.NewReader(f)
for {
r, _, err := br.ReadRune()
if err != nil && !errors.Is(err, io.EOF) {
panic(err)
}
// end of file
if err != nil {
break
}
currentLexer(r)
prevRune = r
escapesNum = 0
}
}
// Find localization strings in lua files
check(filepath.WalkDir(rootPath, func(fp string, fi os.DirEntry, err error) error {
check(err)
if ext := filepath.Ext(fp); ext == ".lua" {
processFile(fp)
}
return nil
}))
fmt.Println("Found localization strings:", localizationStrings.Len())
// Create new or punch holes and filter unused strings from existing locales
for _, locale := range locales {
localePath := filepath.Join(rootPath, "intl", locale+".json")
isNew := true
// Parse old json
oldLocaleData := make(map[string]interface{})
localeContents, err := os.ReadFile(localePath)
if err == nil {
isNew = false
check(json.Unmarshal(localeContents, &oldLocaleData))
} else if !errors.Is(err, os.ErrNotExist) {
check(err)
}
// Merge into new locale for current codebase
var localeData = make(map[string]interface{})
removed := sets.List(sets.New[string](maps.Keys(oldLocaleData)...).Difference(localizationStrings))
untranslated := []string{}
for _, str := range sets.List(localizationStrings) {
if old, ok := oldLocaleData[str]; ok {
localeData[str] = old
} else {
localeData[str] = nil
}
if localeData[str] == nil {
untranslated = append(untranslated, str)
}
}
// Output
resultJson := must(JSONMarshalIndent(localeData, "", "\t"))
check(os.WriteFile(localePath, resultJson, 0644))
fmt.Println()
// Stats
newOrUpdatingMsg := "Updating existing locale"
if len(removed) == 0 && len(untranslated) == 0 {
newOrUpdatingMsg = "Locale is up to date"
} else if isNew {
newOrUpdatingMsg = "Creating new locale"
}
fmt.Println("[[", locale, "]]>", newOrUpdatingMsg)
if len(removed) > 0 {
fmt.Println("• Removed:")
for _, str := range removed {
fmt.Printf(" '%s'\n", str)
}
}
if len(untranslated) > 0 {
fmt.Println("• Untranslated:")
for _, str := range untranslated {
fmt.Printf(" '%s'\n", str)
}
}
}
}
func listFilenamesOfType(directoryPath string, extension string) ([]string, error) {
files := []string{}
extension = strings.ToLower(extension)
dirEntries, err := os.ReadDir(directoryPath)
if err != nil {
return nil, err
}
for _, entry := range dirEntries {
if entry.IsDir() {
continue
}
filename := entry.Name()
ext := filepath.Ext(filename)
if strings.ToLower(ext) == extension {
files = append(files, filename[:len(filename)-len(ext)])
}
}
return files, nil
}
// Because the default `json.Marshal` HTML escapes `&,<,>` characters and it can't be turned off...
func JSONMarshalIndent(t interface{}, prefix string, indent string) ([]byte, error) {
buffer := &bytes.Buffer{}
encoder := json.NewEncoder(buffer)
encoder.SetEscapeHTML(false)
encoder.SetIndent(prefix, indent)
err := encoder.Encode(t)
return buffer.Bytes(), err
}

View File

@@ -0,0 +1,65 @@
package tools
import (
"archive/zip"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"uosc/bins/src/tools/lib"
"k8s.io/apimachinery/pkg/util/sets"
)
func Packager(args []string) {
// Display help.
if len(args) > 0 && sets.New("--help", "-h").Has(args[0]) {
fmt.Printf(`Packages uosc release files into 'release/' directory, while ensuring binaries inside the zip file are marked as executable even when packaged on windows (otherwise this could've just be a simple .ps1/.sh file).`)
os.Exit(0)
}
cwd := must(os.Getwd())
releaseRoot := filepath.Join(cwd, "release")
releaseArchiveSrcDstMap := map[string]string{
filepath.Join(cwd, "src/fonts"): "",
filepath.Join(cwd, "src/uosc"): "scripts/",
}
releaseConfigPath := filepath.Join(releaseRoot, "uosc.conf")
releaseArchivePath := filepath.Join(releaseRoot, "uosc.zip")
sourceConfigPath := filepath.Join(cwd, "src/uosc.conf")
// Naive check binaries are built.
bins := must(os.ReadDir(filepath.Join(cwd, "src/uosc/bin")))
if len(bins) == 0 {
check(errors.New("binaries are not built ('src/uosc/bin' is empty)"))
}
// Cleanup old release.
check(os.RemoveAll(releaseRoot))
// Package new release
var modHeaders lib.HeaderModFn = func(header *zip.FileHeader) *zip.FileHeader {
// Mark binaries as executable.
if strings.HasPrefix(header.Name, "scripts/uosc/bin/") {
header.SetMode(0755)
}
return header
}
stats := must(lib.ZipFilesWithHeaders(releaseArchiveSrcDstMap, releaseArchivePath, modHeaders))
// Copy config to release folder for convenience.
configFileSrc := must(os.Open(sourceConfigPath))
configFileDst := must(os.Create(releaseConfigPath))
confSize := must(io.Copy(configFileDst, configFileSrc))
fmt.Printf(
"Packaging into: %s\n- uosc.zip: %.2f MB, %d files\n- uosc.conf: %.1f KB",
filepath.ToSlash(must(filepath.Rel(cwd, releaseRoot)))+"/",
float64(stats.CompressedBytes)/1024/1024,
stats.FilesNum,
float64(confSize)/1024,
)
}

View File

@@ -2,55 +2,48 @@
timeline_style=line
# Line display style config
timeline_line_width=2
timeline_line_width_fullscreen=3
# Scale the width of the line when minimized (timeline_size_min)
timeline_line_width_minimized_scale=10
# Timeline size when minimized, 0 will hide it completely
timeline_size_min=2
# Timeline size when fully expanded, in pixels, 0 to disable
timeline_size_max=40
# Same as ^ but when in fullscreen
timeline_size_min_fullscreen=0
timeline_size_max_fullscreen=60
# Same thing as calling toggle-progress command once on startup
timeline_start_hidden=no
# Comma separated states when timeline should always be visible. available: paused, audio, image, video, idle
timeline_persistency=paused
# Timeline opacity
timeline_opacity=0.9
timeline_size=40
# Comma separated states when element should always be fully visible.
# Available: paused, audio, image, video, idle, windowed, fullscreen
timeline_persistency=
# Top border of background color to help visually separate timeline from video
timeline_border=1
# When scrolling above timeline, wheel will seek by this amount of seconds
timeline_step=5
# Opacity of chapter indicators in timeline, 0 to disable
timeline_chapters_opacity=0.8
# Render cache indicators for streaming content
timeline_cache=yes
# A comma delimited list of items to construct the controls bar above the timeline. Set to `never` to disable.
# When to display an always visible progress bar (minimized timeline). Can be: windowed, fullscreen, always, never
# Can also be toggled on demand with `toggle-progress` command.
progress=windowed
progress_size=2
progress_line_width=20
# A comma delimited list of controls above the timeline. Set to `never` to disable.
# Parameter spec: enclosed in `{}` means value, enclosed in `[]` means optional
# Full item syntax: `[<[!]{disposition1}[,[!]{dispositionN}]>]{element}[:{paramN}][#{badge}[>{limit}]][?{tooltip}]`
# Common properties:
# `{icon}` - parameter used to specify an icon name (example: `face`)
# - you can pick one here: https://fonts.google.com/icons?selected=Material+Icons&icon.style=Rounded
# - pick here: https://fonts.google.com/icons?icon.platform=web&icon.set=Material+Icons&icon.style=Rounded
# `{element}`s and their parameters:
# `{usoc_command}` - preconfigured shorthands for uosc commands that make sense to have as buttons:
# - `menu`, `subtitles`, `audio`, `video`, `playlist`, `chapters`, `editions`, `stream-quality`,
# `open-file`, `items`, `next`, `prev`, `first`, `last`, `audio-device`
# `fullscreen` - toggle fullscreen
# `loop-playlist` - button to toggle playlist looping
# `loop-file` - button to toggle current file looping
# `shuffle` - toggle for uosc's shuffle mode
# `{shorthand}` - preconfigured shorthands:
# `play-pause`, `menu`, `subtitles`, `audio`, `video`, `playlist`,
# `chapters`, `editions`, `stream-quality`, `open-file`, `items`,
# `next`, `prev`, `first`, `last`, `audio-device`, `fullscreen`,
# `loop-playlist`, `loop-file`, `shuffle`
# `speed[:{scale}]` - display speed slider, [{scale}] - factor of controls_size, default: 1.3
# `command:{icon}:{command}` - button that executes a {command} when pressed
# `toggle:{icon}:{prop}[@{owner}]` - button that toggles mpv property
# `cycle:{default_icon}:{prop}[@{owner}]:{value1}[={icon1}][!]/{valueN}[={iconN}][!]`
# - button that cycles mpv property between values, each optionally having different icon and active flag
# - presence of `!` at the end will style the button as active
# - `{owner}` is the name of a scrip that manages this property if any
# `gap[:{scale}]` - display an empty gap, {scale} - factor of controls_size, default: 0.3
# - button that cycles mpv property between values, each optionally having different icon and active flag
# - presence of `!` at the end will style the button as active
# - `{owner}` is the name of a script that manages this property if any
# `gap[:{scale}]` - display an empty gap
# {scale} - factor of controls_size, default: 0.3
# `space` - fills all available space between previous and next item, useful to align items to the right
# - multiple spaces divide the available space among themselves, which can be used for centering
# `button:{name}` - button whose state, look, and click action are managed by external script
# Item visibility control:
# `<[!]{disposition1}[,[!]{dispositionN}]>` - optional prefix to control element's visibility
# - `{disposition}` can be one of:
@@ -59,6 +52,7 @@ timeline_cache=yes
# - `audio` - true for audio only files
# - `video` - true for files with a video track
# - `has_many_video` - true for files with more than one video track
# - `has_image` - true for files with a cover or other image track
# - `has_audio` - true for files with an audio track
# - `has_many_audio` - true for files with more than one audio track
# - `has_sub` - true for files with an subtitle track
@@ -79,8 +73,7 @@ timeline_cache=yes
# `>{limit}` will display the badge only if it's numerical value is above this threshold.
# Example: `#audio>1`
# Place `?{tooltip}` after the element config to give it a tooltip.
# Example: `<stream>stream-quality?Stream quality`
# Example implementations of some of the available shorthands:
# Example implementations:
# menu = command:menu:script-binding uosc/menu-blurred?Menu
# subtitles = command:subtitles:script-binding uosc/subtitles#sub?Subtitles
# fullscreen = cycle:crop_free:fullscreen:no/yes=fullscreen_exit!?Fullscreen
@@ -88,7 +81,6 @@ timeline_cache=yes
# toggle:{icon}:{prop} = cycle:{icon}:{prop}:no/yes!
controls=menu,gap,subtitles,<has_many_audio>audio,<has_many_video>video,<has_many_edition>editions,<stream>stream-quality,gap,space,speed,space,shuffle,loop-playlist,loop-file,gap,prev,items,next,gap,fullscreen
controls_size=32
controls_size_fullscreen=40
controls_margin=8
controls_spacing=2
controls_persistency=
@@ -96,32 +88,30 @@ controls_persistency=
# Where to display volume controls: none, left, right
volume=right
volume_size=40
volume_size_fullscreen=52
volume_opacity=0.9
volume_border=1
volume_step=1
volume_persistency=
# Playback speed widget: mouse drag or wheel to change, click to reset
speed_opacity=0.6
speed_step=0.1
speed_step_is_factor=no
speed_persistency=
# Controls all menus, such as context menu, subtitle loader/selector, etc
menu_item_height=36
menu_item_height_fullscreen=50
menu_min_width=260
menu_min_width_fullscreen=360
menu_opacity=1
menu_parent_opacity=0.4
menu_padding=4
# Determines if `/` or `ctrl+f` is required to activate the search, or if typing
# any text is sufficient.
# When enabled, you can no longer toggle a menu off with the same key that opened it, if the key is a unicode character.
menu_type_to_search=yes
# Top bar with window controls and media title
# Can be: never, no-border, always
top_bar=no-border
top_bar_size=40
top_bar_size_fullscreen=46
top_bar_controls=yes
# Can be: `no` (hide), left or right
top_bar_controls=right
# Can be: `no` (hide), `yes` (inherit title from mpv.conf), or a custom template string
top_bar_title=yes
# Template string to enable alternative top bar title. If alt title matches main title,
@@ -132,12 +122,12 @@ top_bar_alt_title=
# `toggle` => toggle the top bar title text between main and alt by clicking
# the top bar, or calling `toggle-title` binding
top_bar_alt_title_place=below
top_bar_title_opacity=0.8
# Flash top bar when any of these file types is loaded. Available: audio,video,image,chapter
top_bar_flash_on=video,audio
top_bar_persistency=
# Window border drawn in no-border mode
window_border_size=1
window_border_opacity=0.8
# If there's no playlist and file ends, load next file in the directory
# Requires `keep-open=yes` in `mpv.conf`.
@@ -151,14 +141,30 @@ autoload_types=video,audio,image
shuffle=no
# Scale the interface by this factor
ui_scale=1
scale=1
# Scale in fullscreen
scale_fullscreen=1.3
# Adjust the text scaling to fit your font
font_scale=1
# Border of text and icons when drawn directly on top of video
text_border=1.2
# Use a faster estimation method instead of accurate measurement
# setting this to `no` might have a noticable impact on performance, especially in large menus.
text_width_estimation=yes
# Border radius of buttons, menus, and all other rectangles
border_radius=4
# A comma delimited list of color overrides in RGB HEX format. Defaults:
# foreground=ffffff,foreground_text=000000,background=000000,background_text=ffffff,curtain=111111,success=a5e075,error=ff616e
color=
# A comma delimited list of opacity overrides for various UI element backgrounds and shapes.
# This does not affect any text, which is always rendered fully opaque. Defaults:
# timeline=0.9,position=1,chapters=0.8,slider=0.9,slider_gauge=1,controls=0,speed=0.6,menu=1,submenu=0.4,border=1,title=1,tooltip=1,thumbnail=1,curtain=0.8,idle_indicator=0.8,audio_indicator=0.5,buffering_indicator=0.3,playlist_position=0.8
opacity=
# A comma delimited list of features to refine at a cost of some performance impact.
# text_width - Use a more accurate text width measurement that measures each text string individually
# instead of just measuring the width of known letters once and adding them up.
# sorting - Use filename sorting that handles non-english languages better, especially asian ones.
# At the moment, this is only available on windows, and has no effect on other platforms.
refine=
# Duration of animations in milliseconds
animation_duration=100
# Execute command for background clicks shorter than this number of milliseconds, 0 to disable
# Execution always waits for `input-doubleclick-time` to filter out double-clicks
click_threshold=0
@@ -168,11 +174,6 @@ flash_duration=1000
# Distances in pixels below which elements are fully faded in/out
proximity_in=40
proximity_out=120
# RGB HEX color codes
foreground=ffffff
foreground_text=000000
background=000000
background_text=ffffff
# Use only bold font weight throughout the whole UI
font_bold=no
# One of `total`, `playtime-remaining` (scaled by the current speed), `time-remaining` (remaining length of file)
@@ -181,21 +182,23 @@ destination_time=playtime-remaining
time_precision=0
# Display stream's buffered time in timeline if it's lower than this amount of seconds, 0 to disable
buffered_time_threshold=60
# Hide UI when mpv autohides the cursor
# Hide UI when mpv autohides the cursor. Timing is controlled by `cursor-autohide` in `mpv.conf` (in milliseconds).
autohide=no
# Can be: none, flash, static, manual (controlled by flash-pause-indicator and decide-pause-indicator commands)
# Can be: flash, static, manual (controlled by flash-pause-indicator and decide-pause-indicator commands)
pause_indicator=flash
# Screen dim when stuff like menu is open, 0 to disable
curtain_opacity=0.5
# Sizes to list in stream quality menu
stream_quality_options=4320,2160,1440,1080,720,480,360,240,144
# Types to identify media files
video_types=3g2,3gp,asf,avi,f4v,flv,h264,h265,m2ts,m4v,mkv,mov,mp4,mp4v,mpeg,mpg,ogm,ogv,rm,rmvb,ts,vob,webm,wmv,y4m
audio_types=aac,ac3,aiff,ape,au,dsf,dts,flac,m4a,mid,midi,mka,mp3,mp4a,oga,ogg,opus,spx,tak,tta,wav,weba,wma,wv
audio_types=aac,ac3,aiff,ape,au,cue,dsf,dts,flac,m4a,mid,midi,mka,mp3,mp4a,oga,ogg,opus,spx,tak,tta,wav,weba,wma,wv
image_types=apng,avif,bmp,gif,j2k,jp2,jfif,jpeg,jpg,jxl,mj2,png,svg,tga,tif,tiff,webp
subtitle_types=aqt,ass,gsub,idx,jss,lrc,mks,pgs,pjs,psb,rt,slt,smi,sub,sup,srt,ssa,ssf,ttxt,txt,usf,vt,vtt
# Default open-file menu directory
subtitle_types=aqt,ass,gsub,idx,jss,lrc,mks,pgs,pjs,psb,rt,sbv,slt,smi,sub,sup,srt,ssa,ssf,ttxt,txt,usf,vt,vtt
playlist_types=m3u,m3u8,pls,url,cue
# Default open-file menu directory. Use `{drives}` to open drives menu on windows (defaults to `/` on unix).
default_directory=~/
# List hidden files when reading directories. Due to environment limitations, this currently only hides
# files starting with a dot. Doesn't hide hidden files on windows (we have no way to tell they're hidden).
show_hidden_files=no
# Move files to trash (recycle bin) when deleting files. Dependencies:
# - Linux: `sudo apt install trash-cli`
# - MacOS: `brew install trash`
@@ -221,7 +224,13 @@ chapter_ranges=openings:30abf964,endings:30abf964,ads:c54e4e80
chapter_range_patterns=openings:オープニング;endings:エンディング
# Localization language priority from highest to lowest.
# Also controls what languages are fetched by `download-subtitles` menu.
# Built in languages can be found in `uosc/intl`.
# `slang` is a keyword to inherit values from `--slang` mpv config.
# Supports paths to custom json files: `languages=~~/custom.json,slang,en`
languages=slang,en
# A comma separated list of element IDs to disable. Available IDs:
# window_border, top_bar, timeline, controls, volume,
# idle_indicator, audio_indicator, buffering_indicator, pause_indicator
disable_elements=

405
src/uosc/char-conv/zh.json Normal file
View File

@@ -0,0 +1,405 @@
{
"a": "阿啊呵腌嗄锕錒",
"ai": "爱唉挨碍哀矮埃哎艾癌隘蔼嗳皑霭捱暧瑷娭砹锿嫒薆䔽㤅鴱㗨藹㕌磑礙硋䑂愛壒㘷叆靉䨠毐塧靄璦㱯䶣瞹䀳濭溰溾曖昹啀噯嘊㗒㝶䝽敱敳賹懓懝㢊銰鑀鱫鎄皧皚馤躷䅬㿄凒娾嬡伌㑸僾餲䬵譪譺",
"an": "安按案暗岸氨俺铵胺鞍黯庵桉谙鮟鹌咹犴广厂埯揞菴蓭荌萻葊隌㸩䮗㱘鵪豻貋腤雸堓垵䎨玵䁆洝晻唵啽㽢罯㟁屵䯥峖鞌䅁錌銨䅖馣痷鶕闇媕㜝盫儑侒䬓偣韽盦䎏䜙諳誝",
"ang": "昂肮盎䩕䒢䭹䭺䀚昻㦹㼜骯岇醠㭿枊䍩仰",
"ao": "傲熬凹遨嗷奥拗澳袄懊坳敖翱螯鳌鏖岙媪鏊骜艹聱獒廒蔜芺隞隩䮯厫磝䦋奡镺䐿䞝垇㘬墺㘭璈嗸嶅䫨㥿驁鰲鷔䵅摮嫯鼇謷䁱滶澚㕭軪䯠㟼㠂㠗岰慠㤇爊襖䥝獓翶擙抝䚫梎柪翺嶴䴈奧㿰㜜媼㜩㑃謸䜒",
"ba": "把八吧巴爸罢拔霸坝叭芭扒跋疤靶耙粑笆钯伯茇菝灞岜鲅捌魃䩻䩗夿䳊䃻㔜胈鼥壩垻豝玐㶚蚆㖠跁䟦哵罷軷㞎炦鈀鲃䥯䰾鮁䳁䱝魞釟抜㧊扷覇柭欛朳叐矲䆉羓䇑丷妭颰癹仈弝詙",
"bai": "白百摆败拜柏呗掰伯稗捭佰薭䒔㼣㓦瓸㗗㗑贁㠔䢙敗韛粨庍粺䙓襬猈拝㼟擺䳆挀㿟㧳栢䴽竡絔",
"ban": "办般半班板伴版搬斑扮颁瓣拌扳绊坂阪舨瘢柈钣癍靽䕰㚘瓪湴昄蝂岅肦怑粄䉽魬鈑鉡㩯㸞秚褩螌辦㪵闆辬姅頒鳻攽䬳絆斒",
"bang": "帮邦膀棒傍榜绑梆磅蚌旁镑谤浜蒡䧛䂜幚邫垹鞤幇幫䎧塝玤蜯䖫䟺髈䰷鎊挷捠搒㯁㭋㮄棓牓稖艕㔙㾦綁縍謗彭",
"bao": "保报包胞宝暴抱薄剥炮爆饱堡孢豹瀑刨鲍苞雹葆曝褒鸨褓煲龅趵勹蕔菢藵犦髱䨌䨔㙅靌報堢㙸虣珤㻄齙㵡㿺曓㫧蚫骲鳵怉䎂寳寚寶䴐宲窇䥤鑤㲒鮑䳈鉋铇勽䤖枹䈏㲏忁笣䪨闁媬儤賲䳰佨飹飽䭋駂鴇剝緥袌裦襃",
"bei": "被北备背倍悲贝杯辈臂卑碑呗狈惫钡悖孛蓓焙陂碚褙勃鞴鐾庳鹎邶䔒鞁藣苝犕㸬㸽牬盃愂䎬䎱琲㰆珼䁅輩䩀㻗㶔昁蛽䠙唄䡶㽡郥骳貝㤳糒禙鋇䰽狽㔨鉳㼎鵯揹柸桮梖椑㸢㓈㾱鄁軰㛝僃備憊㷶偹俻偝㣁䋳誖",
"ben": "本奔笨苯贲坌夯畚锛奙逩坋㱵渀漰泍㤓㡷錛撪㨧捹桳㮺翉楍栟犇倴䬱",
"beng": "崩蹦绷甭迸泵嘣甏菶䩬鞛奟䳞埲䨻塴埄琫㱶琣嗙嵭㷯䙀祊鏰镚甮揼逬䭰痭閍伻㑟綳䋽繃絣蚌",
"bi": "比必笔毕避壁秘闭鼻币彼逼辟臂泌碧弊蔽鄙毙弼痹庇陛璧婢敝匕俾裨荸吡哔蓖贲襞铋秕毖愎髀篦睥畀妣筚薜萆芘荜滗濞跸嬖狴箅舭鞸䩛蓽㳼萞苾䕗䎵聛䧗驆駜䮡夶髲䭮觱㗉皕䏢腷毴貏䏶賁堛䟆㙄㘩㻫豍珌㱸㻶㹃睤䁹䀣湢滭幤㵥獘斃鄨幣鷩潷䨆沘㡀畢鷝㪤䖩螕蜌啚蹕䟤躃䠋嗶咇罼奰㘠貱䯗畁㡙㠲贔赑怶愊韠䪐躄繴㵨鼊怭屄邲煏熚廦䊧粃襅袐襣禆䃾鲾鏎鐴鉍鰏鮅獙鎞㧙魓㪏柀楅䣥㯇㮿柲榌㮰䵄朼梐䫁篳馝䇷箆筆䄶閉閇㓖閟痺䦘疕疪㚰妼鵖嬶佊偪朇佖䬛䫾饆飶㢰䋔㢶弻彃縪鄪䌟綼㿫毞坒粊㢸䘡詖诐佛拂",
"bian": "变便边遍编辩辨鞭扁贬辫蝙匾卞鳊汴砭弁苄碥忭煸褊窆笾缏䒪鞕䛒藊萹䪻鴘㺹玣㻞䁵㳎覍汳㝸㴜昪䡢峅貶惼炞糄鯾鯿獱猵鍽㣐抃揙㭓牑邉邊釆籩艑䉸箯徧稨閞㵷㦚辡辧辮辯緶編㲢甂変諚變",
"biao": "表标彪裱婊飚飙镖膘鳔俵骠镳飑髟瘭䔸藨蔈骉驫飆猋嫑磦脿爂臕墂㯱滮淲瀌贆幖㟽㠒飊熛褾錶鑣鏢㧼摽㯹標檦穮儦飇颩颮颷飈諘謤䞄",
"bie": "别憋瘪鳖蹩虌莂䏟蟞鱉龞彆鼈蛂䠥別襒䋢徶䭱䉲癟㿜㢼",
"bin": "宾滨斌彬濒缤鬓槟殡摈膑邠玢份频髌豳镔傧髩鬢鬂臏䐔霦豩璸瑸殯頻虨瀕濱濵汃髕賓顮䚔賔鑌擯檳梹椕儐繽",
"bing": "并病兵冰饼丙柄炳秉禀屏槟摒邴鞆鞞苪䓑陃靐垪眪昞昺蛃䗒怲庰寎窉鈵鮩鋲鉼掤抦㨀棅栤䴵幷䈂並竝偋倂併仒傡餠餅仌䋑氷稟誁",
"bo": "波伯播剥博玻勃拨柏脖卜搏泊驳膊舶簿渤簸菠箔跛薄钵铂僰帛礴饽钹亳啵檗鹁踣擘䪇葧萡蘗蔔㹀䂍䮂䮀駁駮驋礡盋䰊䫊㝿砵䶈肑胉䑈䞳郣鵓㪍䢌㱟碆浡㴾溊淿謈㬧㬍䗚䟛蹳嚗㗘㖕䯋髉髆㟑嶓懪孹糪愽㶿煿袹襮袯䙏襏鑮䥬鈸鋍鲌馎鮊鱍鉑鎛镈鉢狛猼瓟瓝㩭挬㩧撥欂桲秡䢪缽簙牔䭯馛馞䒄艊䍨䪬䍸癷侼癶仢僠䭦䬪餑餺紴䊿袰譒佛",
"bu": "不部步布补捕卜哺埔怖簿埠钚卟逋晡钸醭瓿䪁荹蔀䏽㘵埗㙛㻉歨歩堡䴝鳪䀯㳍吥咘踄轐峬䝵䪔悑庯䊇廍補鈈鈽錻鸔抪捗㨐柨鵏䴺䍌䒈䑰篰㾟勏郶䳝㚴佈䬏餔餢䋠誧",
"ca": "擦嚓礤遪礸䟃䵽攃",
"cai": "才采材财彩菜裁猜蔡踩睬䰂毝䐆埰䞗啋跴財㥒寀採棌䴭䣋㒲婇倸偲䌨綵䌽纔縩",
"can": "参残餐惨蚕灿掺惭璨孱粲骖黪薒䣟朁蠶叄參㕘叅驂䏼蝅蠺䗞䘉殘㱚㻮㣓䝳㛑澯湌㘔喰㽩黲慙䳻㨻㥇憯慘慚㦧燦爘䙁䗝㺑穇䅟䑶㿊䍼飡䫮㜗嬠傪儏䬫謲䛹",
"cang": "藏苍仓舱沧臧伧蒼㶓㵴濸滄螥嵢賶鑶獊欌艙䅮凔仺鸧傖倉鶬䢢",
"cao": "草操曹槽糙嘈漕螬艚蓸䏆艸騲䐬鼜䎭曺鄵嶆愺慅慒懆褿襙䄚鏪撡㯥肏䒑㜖",
"ce": "策测侧册厕栅恻拆䔴荝萴萗蓛厠䜺測畟冊㥽夨惻憡廁粣䊂㨲拺㩍敇筴䇲䈟笧筞簎箣側",
"cen": "参岑涔䃡䨙埁㻸嵾䯔㞥䲋䤁䅾篸笒",
"ceng": "层曾蹭噌驓䁬㬝嶒層䉕㣒竲",
"cha": "差察查茶插叉诧岔刹喳茬嚓楂杈碴汊搽衩姹槎馇镲锸猹檫靫䕓䓭䒲䰈䑘垞䶪䁟嗏䟕蹅嵖㣾㤞㢒㢎㢉䆛銟䲦䤩鑔鍤扠剎挿揷査臿䊬秅䑡艖䡨疀奼侘偛餷詧紁㪯詫㛳㫅",
"chai": "差柴拆钗豺侪虿瘥茝芆䓱蠆䘍袃肞㼮祡㳗囆喍釵犲㾹儕㑪訍",
"chan": "产颤阐缠禅铲掺潺馋蝉搀蟾忏谄孱谗巉廛羼崭蒇骣觇澶躔冁婵单剗蕆䩶韂䵐苂䧯㹌䣑硟䐮䑎壥㙻㶣㙴刬䀡覘㢟䂁湹瀍瀺潹㵌灛滻浐㬄蟐蟬螹旵䠨囅丳嚵䡲磛䡪幝幨辿嵼懴䪜㦃懺煘鄽㢆燀裧襜䥀酁劖毚䤫䱿鑱镵㹽鋋鋓獑㺥鏟摻摲攙摌醦䤘䊲棎欃梴㯆䴼㸥艬簅闡閳産剷嬋儳饞儃緾繟纏纒產譂顫諂䜛讒誗讇斺",
"chang": "长常场厂唱昌肠偿尝倡畅倘敞淌猖怅嫦娼氅菖昶徜鲳惝苌鬯阊伥萇䩨䕋長䯴镸瓺兏厰腸膓㙊場鼚塲瑺瑒琩玚仧淐甞嘗㦂䗅暢㫤䠆䠀嚐畼悵韔廠焻裮鋹鲿錩锠鱨鯧椙閶倀仩僘償誯裳",
"chao": "朝超潮吵巢抄嘲剿炒钞绰晁焯耖怊焣㷅䏚䎐眧巣漅鼌鼂罺轈巐䬤煼㶤窲窼觘鈔䰫樔麨牊䄻鄛欩仯仦弨謿訬",
"che": "车彻撤扯澈掣尺屮砗坼莗㱌䧪聅䨁硨䞣䰩頙迠瞮䁤㵔蛼㬚唓車㥉爡烢䚢撦㨋硩㿭㯙䑲䒆勶徹㾝偖伡俥㔭䋲䛸䜠",
"chen": "称陈沉晨臣尘趁衬辰嗔琛抻伧谶碜宸郴谌忱龀榇茞蔯莀䢻莐薼䒞陳螴敶磣䣅㲀㫳䢈敐硶䫖夦䢅䐜墋趂霃齓齔瞋㴴鷐迧曟踸䟢趻㕴嚫軙贂賝䞋愖煁麎塵襯䆣鍖鈂䤟捵栕樄桭梣棽櫬䚘䑣瘎疢㽸㧱儭諶䜟謓訦諃讖沈",
"cheng": "成程称城承诚呈乘惩撑澄秤橙逞丞骋盛瞠铛塍柽埕琤净抢蛏裎铖酲枨荿䔲䧕阷㞼騁䮪騬郕䫆㼩碀脭爯頳䞓赪赬塖堘珹靗珵睈䀕䁎洆浾泟澂㲂牚瀓溗蟶晿䗊畻峸憆悜憕庱宬窚竀䆑䆵䆸䄇鋮鐣鏿鯎掁摚撐挰㨃揨檉棖檙橕棦朾乗筬稱罉穪䇸徎懲娍偁侱㐼饓僜絾緽誠椉",
"chi": "吃持池尺赤迟斥齿翅驰耻痴弛炽哧侈嗤叱敕啻饬笞踟柢呎茌褫鸱勅墀蚩蚩豉眵螭魑匙篪瘛媸傺荎䠠㔑䔟䧝妛恥欼馳䮻䮈肔胵腟胣䐤趩赿䞾灻垑漦雴鵄䜵䜻彨彲銐殦䶔齝齒歯瞝懘㳏湁蚇蚳喫噄䟷翤叺㽚㞿㞴貾㟂恜翄㓾遲翨杘遅遟憏迡烾㢁㶴粚㡿㢋熾裭䙙袳鴟㱀䤲鍉卶鉹㺈鉓瓻䰡抶摛攡㮛鶒慗鷘遫㓼麶勑䳵竾䑛䇪箎筂䈕䇼黐䪧痸癡侙䶵伬䬜飭㒆㘜䊼絺㢮訵㙜䜄謘袲誺䛂",
"chong": "重充冲虫崇涌宠憧忡舂铳种茺艟隀憃埫珫沖漴浺㳘蟲蝩蹖嘃罿㓽翀爞崈寵褈銃摏揰㧤䳯䖝衝㹐緟䌬",
"chou": "抽筹仇丑愁臭酬畴瞅绸稠踌惆帱瘳俦雠䓓薵菗䔏遚魗㦞㐜殠矁㵞躊吜疇幬㤽懤燽䊭裯䲖鮘㿧㩅皗搊㨶梼檮醻酧醜椆杽栦籌䇺臰篘䪮嬦㛶丒儔䀺偢犨讐雔雦犫讎䌧綢紬䌷絒詶",
"chu": "出处除初础助楚触畜储厨锄橱雏躇矗搐刍蜍怵滁黜绌杵蹰亍憷樗楮蒢蒭䢺㔘欪䧁䮞犓㕑㕏礎貙臅㙇埱趎耡䎤㼥䎝豠豖珿䜴璴齣齭齼敊䖏處泏濋㶆滀蟵䟞䠧躕䟣嘼㗰歜㡡幮岀㤕㤘廚䊰䙘䙕禇鶵芻雛鋤鉏㐥觸㹼摴斶櫉櫥䠂椘檚榋篨䅳処䦌竐竌閦媰俶儊儲傗絀諔鄐",
"chuai": "揣啜踹膪搋膗㪜㪓䦤䦟䦷",
"chuan": "传船穿川串喘椽氚钏舛遄舡巛荈堾玔瑏㱛䁣汌暷踳圌輲歂㼷賗釧猭㯌篅舩僢傳剶鶨",
"chuang": "创床窗闯疮怆磢䃥䚎刱䎫㵂噇䡴㡖愴窓窻摐牎牕䇬剙剏闖䚒牀瘡刅傸䭚創幢",
"chui": "吹垂锤椎炊捶槌陲棰菙㝽腄䞼䶴㓃錘鎚搥桘㩾䳠䍋埀䄲箠㥨龡倕顀",
"chun": "春纯唇醇蠢淳椿莼鹑蝽䔚䓐萶萅蓴蒓陙㸪犉脣䫃惷䐏䏝䐇䏛旾瑃睶㵮浱漘滣湻暙㖺輴賰䞐䄝䥎鰆鯙錞㿤鶞槆杶䣩䣨醕櫄橁箺䦮媋偆純㝄鶉㝇",
"chuo": "戳绰辍龊啜淖踔辶䓎歠䮕磭䃗辵趠繛齪逴涰嚽踀哾輟惙䆯鑡㚟㲋擉酫䂐䄪䍳䇍婥娖娕餟䋘綽",
"ci": "此次差词刺磁辞雌慈兹瓷赐伺疵呲糍祠茨鹚䓧㹂茦莿薋䦻㤵辝䰍䯸䂣礠㓨辭辤蛓趀䨏珁玼刾䧳㘹䖪㠿鮆鴜䳄飺泚濨蠀䗹螆跐㘂骴髊賜䛐㞖䲿㡹庛㢀皉㩞朿柌栨䆅䈘齹垐䳐餈鶿鷀甆嬨佌偨佽䭣縒絘詞",
"cong": "从匆丛聪葱囱淙熜琮苁骢璁枞藂䕺茐蔥蓯孮聦聰聡騘驄瑽瞛潈潀灇潨漗漎蟌暰䟲賩悰愡憁爜叢賨錝鍯怱鏓鏦欉樷樬樅棇徔悤囪徖䉘篵従從䳷㼻婃忩繱誴謥",
"cou": "凑腠辏楱湊㫶輳",
"cu": "促粗簇醋卒蹙猝蹴徂趣趋蔟殂䓚觕㗤顣䃚䢐脨趗鼀䠞踧踿䠓噈怚䎌憱麤䙯䥄麁䥘䟟㰗橻瘄瘯媨麄䬨縬蹵䛤誎",
"cuan": "窜篡攒蹿撺爨镩汆䰖㸑殩㵀躥㠝巑熶竄䆘鑹攛櫕欑㭫簒穳",
"cui": "催翠脆粹崔摧萃悴瘁璀啐淬毳榱䃀磪䂱膵膬䄟㯔臎脃脺趡墔琗㧘㱖㵏漼濢㳃啛嵟慛㥞忰翆㷃䊫粋㷪焠㝮襊竁鏙皠㯜槯䧽䆊凗疩伜倅紣縗缞綷顇衰",
"cun": "存村寸忖皴吋刌壿邨膥澊踆籿拵䍎竴侟",
"cuo": "错措挫搓撮磋锉蹉矬厝脞鹾鹾嵯痤蔖剒逪莡蒫莝遳蓌䂳䐣瑳䣜虘鹺睉䠡䟶㽨嵳㟇錯䱜鎈銼醝䴾酂酇㿷剉夎",
"da": "大打达答搭瘩嗒哒鞑沓耷惮靼跶褡怛笪妲荙韃䩢薘剳荅䃮迖羍迏䐛䐊垯墶㙮逹達溚蟽噠迚呾咑䵣䳴眔㟷燵炟匒鎝鐽鎉撘㯚笚䑽龖龘㾑㜓㿯畣繨詚亣畗",
"dai": "代带待袋戴呆贷逮歹岱傣玳怠黛殆迨甙棣呔诒埭毒大绐帶䒫貣㞭黱叇霴靆瑇帯㻖瀻蝳㫹曃蚮蹛跢軩軑轪軚獃懛廗襶䚟䚞鴏㯂簤艜䈆㿃垈帒貸柋㐲侢㶡紿緿",
"dan": "但单担弹蛋淡胆丹旦氮诞耽郸掸惮疸眈赕澹啖箪膻石萏聃殚瘅儋蓞䩥匰耼聸馾駳髧砃䃫㽎腅膽䨢霮䨵玬殫頕㴷単泹㵅鴠㫜啿㗖鄲單噉㕪啗嘾唌嚪黮黕黵帎賧贉刐饏疍憚憺㡺瓭沊㱽褝襌衴窞禫甔觛䱋狚㺗撣㲷抌擔撢酖柦䄷䉞蜑簞䉷躭癉癚媅妉僤伔䭛餤弾彈紞繵訑勯亶㔊誕",
"dang": "当党荡挡档裆铛宕噹菪砀凼谠蘯蕩礑碭䑗雼圵趤壋垱璫珰瞊澢灙盪璗䣊䣣當黨瓽潒逿蟷嵣氹愓襠鐺擋攩檔欓簜簹筜艡䦒闣㜭婸儅譡讜",
"dao": "到道导倒岛刀蹈稻盗捣叨悼祷焘氘捯纛刂忉菿陦隯﨩隝䧂䲽壔翿燾瓙盜螩翢嶹嶌嶋禱禂鱽島㠀魛釖擣搗椡槝檤朷稲軇艔衜舠衟㿒導噵䆃辺䌦",
"de": "的地得德嘚底锝㤫悳惪㥁䙸䙷淂㝵㥀鍀㯖棏徳恴",
"dei": "得嘚",
"deng": "等灯登邓瞪凳澄蹬噔磴戥嶝镫簦䒭隥䮴墱璒䠬燈鐙櫈艠竳嬁鄧㲪覴豋",
"di": "的地第提低底敌帝弟抵递滴迪堤蒂缔笛涤狄嘀谛娣嫡邸诋砥棣碲柢睇骶荻觌坻氐镝籴羝蔕䩘鞮靮䩚蔋苖菧慸遰菂苐蔐藡隄聜阺墬埅䮤馰牴㹍髢䯼磾厎奃䂡腣坘䞶趆覿䨤埞墑䶍豴玓珶眱䴞䀿坔滌螮蝃㼵䗖蝭旳踶䟡蹢嚁呧唙啲䵠軧䍕頔嶳埊廸岻怟鸐䊮䣌㡳焍袛祶禘鉪㪆䢑釱觝䏑鯳䱃䱱鏑摕逓遞掋拞䀸梊杕枤㭽梑樀楴㰅㣙彽秪䑭䑯糴䨀媂僀仾俤偙弤㢩締詆啇敵甋遆諦翟",
"dia": "嗲",
"dian": "点电典店淀颠殿垫奠甸碘佃滇惦巅癫掂踮玷靛钿癜阽坫簟蒧蕇䓦䧃驔厧磹㼭䟍顛㒹電墊琔齻奌敁㓠澱㵤㶘蜔蹎跕嚸㸃點敟巔嵮巓壂㞟㥆㝪鈿攧槙椣橂槇䍄癲瘨㚲婰婝傎顚扂",
"diao": "掉雕吊钓刁叼调碉凋貂鲷屌铞铫藋䔙蓧䂽奝䂪鼦雿琱㪕瞗汈蛁虭䵲彫鵰䘟窎窵鋽銱錭鑃鯛魡鮉銚釣㹿鈟扚䠼簓䉆竨瘹刟鳭㒛伄弔盄弴調訋",
"die": "爹跌叠蝶迭碟谍喋牒堞蹀垤耋鲽瓞㦶戜苵㲲䴑䮢镻胅䏲臷趃䞕耊褺䠟䲀䞇㻡殜眰眣蜨曡㬪螲㫼昳哋咥跮疂氎疊疉畳嵽峌幉㥈惵㦅恎㷸褋䘭㲳鰈䳀挕㩹㩸楪㭯鴩艓牃㑙絰绖諜詄佚",
"ding": "定顶丁订钉盯叮鼎锭啶腚仃町铤酊疔碇耵玎靪薡萣艼聢䦺矴磸碠鼑濎㴿㫀蝊虰帄嵿忊顁㝎鐤饤錠釘頂㼗㐉椗奵飣訂",
"diu": "丢铥丟銩",
"dong": "动东冬洞懂冻董咚栋侗峒恫胴氡鸫硐胨垌岽菄苳蕫駧䂢腖霘鼕䞒埬涷湩蝀昸㖦㗢戙迵㢥崬崠鯟鮗挏氭㨂東㼯鶇鶫棟動徚䅍箽笗㐑䳉䵔㓊凍䍶嬞姛㜱娻㑈倲働諌",
"dou": "都斗读豆抖兜陡逗窦蚪痘渎吋蔸篼钭䕱荳䕆阧脰郖毭㪷㐙鬦鬪鬥鬬鬭浢唗唞吺斣㞳㢄㷆竇䄈饾鈄㨮兠梪酘橷枓乧闘閗㛒餖䬦䛠",
"du": "度独读毒督渡杜肚堵赌嘟笃睹妒都镀竺犊渎牍蠹黩阇芏髑椟靯韇䪅匵䓯荰犢㸿騳䮷䀾䐗皾䢱蠧䲧覩剢瓄琽㱩殰殬裻錖瀆涜䟻黷䫳賭厾韣韥䙱䄍鑟鍍獨贕櫝醏螙篤牘䅊秺䈞凟闍㾄妬嬻豄讀讟読",
"duan": "断段短端锻缎煅椴簖葮碫腶塅㱭瑖躖䠪耑褍鍴鍛毈籪媏偳緞斷㫁",
"dui": "对队堆兑碓敦追怼镦憝䔪薱隊陮磓䨺䨴垖塠㙂㳔㵽濧瀩㬣轛䯟㠚㟋憞䊚對懟祋鐓䇤頧鴭痽䇏兊兌䬈䬽綐鐜対譵譈",
"dun": "盾顿吨蹲敦钝墩囤沌遁盹炖趸惇砘礅躉驐犜碷遯㬿逇頓潡蜳噸踲蹾㥫庉燉鈍䤜獤撴伅墪撉",
"duo": "多夺朵躲踱度堕惰哆舵跺垛咄掇铎剁哚柁裰缍䩔䩣䒳墮陏陊刴朶敠毲剟鵽敪鬌奲尮奪䐾垜㙍趓㙐埵㻧㻔畓㖼跥䠤喥嚉崜憜墯㥩剫䙃䙟䙤䤻鐸饳鈬䫂䤪挅㧷挆柮桗椯㔍軃躱䅜䑨㣞敚凙䍴痥㛆夛㛊敓飿綞嚲亸䯬隋",
"e": "额恶俄饿呃鹅扼厄蛾娥峨愕鳄鄂遏萼腭颚讹噩谔婀锷垩轭屙阿咹鹗苊莪锇䩹䳬䓊㼢蕚䔾䕏阨鵈娿阸騀頋阏砐砈㕎礘磀硆砨㼂妿䞩堨堮迗䝈豟堊蝁惡琧悪䫷㱦珴齶歺睋湂涐蚅歞噁卾㓵顎咢鶚遌覨㗁䣞遻㖾吪呝軛囮軶岋㡋崿㟧㠋㟯峉峩㦍㷈廅額頞䆓䄉鈪匎㔩鑩鍔䱮鰪鱷鰐䳗䳘魤鋨鈋擜搹㩵㼰皒搤㧖枙櫮㮙䙳齃頟䖸鵝鵞䑥䑪閼妸姶僫偔餓餩譌讍䛖諤戹誐訛哦",
"ei": "诶欸",
"en": "嗯恩摁蒽奀峎䊐煾䅰䭡䭓䬶",
"er": "而二儿尔耳饵迩洱贰鲕珥鸸铒佴荋貳弍薾聏陑毦隭刵䎶駬䮘髶髵耏鴯䏪胹兒趰弐貮邇爾児洏咡㖇唲輀轜峏粫袻鉺鮞㧫樲栮㮕栭䣵尓衈㛅䎟㜨㚷䎠㒃侕尒餌䋙䌺㢽䋩誀",
"fa": "发法乏罚伐阀筏砝珐垡䒥藅茷蕟髪髮䂲坺㘺墢琺沷㳒灋浌㕹罸罰峜彂鍅瞂䣹栰橃笩䇅冹疺閥㛲姂佱発發傠",
"fan": "反范饭犯翻繁凡泛番烦返贩帆藩梵樊蕃矾幡钒畈璠蘩燔蹯匥薠䒦㝃軬䮳颿䭵膰䐪墦䪤凣䀟㴀䀀氾滼瀿盕汎噃㕨輽䡊轓軓㠶販䪛㤆憣忛煩籵畨䊩襎㼝鱕㸋鐇㺕釩払䣲礬蠜䫶鐢棥橎柉杋笲䉊笵籓範勫飜鷭䉒舧舤凢瀪緐䌓㶗䋣㽹羳嬎㜶嬏奿仮飯飰繙䋦䛀旙旛訉拚",
"fang": "方放房防仿访芳纺妨肪坊彷舫鲂钫匚枋邡㯐牥䦈髣眆淓汸昘昉蚄趽㕫㤃錺魴䲱鈁㧍堏㑂倣鶭紡瓬䢍鴋旊訪",
"fei": "非飞费肥废肺匪菲沸啡妃吠斐翡诽绯蜚扉霏腓痱悱芾榧狒淝鲱镄镄篚萉蕜䕁䕠陫騑騛䰁厞朏蜰䑔鼣胇靅奜猆靟䩁剕㐟䨽棐婓餥渄濷㵒蟦暃昲曊䠊胐㥱屝飛飝䨾廃廢裶䚨䤵鯡鐨㩌杮㭭櫠䈈馡䆏䉬癈疿婔俷緋㔗費誹䛍",
"fen": "分份奋粉纷愤氛芬粪坟焚吩酚忿汾雰玢鼢瀵鲼棼偾蕡䩿棻蒶隫㸮奮膹朌鼖䴅墳豮豶瞓濆昐蚡㖹轒幩帉岎憤翂燌黺糞黂㥹衯鐼鱝魵獖鈖㮥橨梤燓㷊枌馩馚躮秎羵㿎朆竕羒妢僨弅餴饙蚠炃紛䯨訜",
"feng": "风封丰锋峰奉凤缝蜂冯逢疯讽枫沣烽俸砜葑唪酆䒠䩼飌蘴碸䏎堼犎霻靊堸鴌琒盽湗灃溄浲漨㵯沨渢䟪鄷豐崶㡝賵赗峯㦀焨煈寷䙜鎽鋒鏠猦摓檒桻覂楓麷夆蠭㷭篈艂馮瘋妦仹凮凨凬鳳僼鳯風偑綘縫諷",
"fo": "佛坲梻仏",
"fou": "否不缶鴀䳕雬殕缹缻妚紑",
"fu": "复服夫富府父负副福妇附符付幅伏浮腐腹傅扶辐肤抚覆辅赋赴甫缚弗咐俯俘孵拂斧敷脯腑袱芙氟孚蝠阜匐麸釜涪馥凫驸茯讣蝮蚨苻呋罘稃芾跗拊茀趺伕鄜莩菔莩阝砩郛滏蜉呒幞赙赙怫黻黼祓鳆鲋桴绂艴绋荂芣葍䕎䓛䔰萯荴蕧䧞䮛駙䭸䯱㬼䯽髴砆䩉㕊䂤㚕鵩胕䨗䞜䞯䞸䞞韨䘄㙏䨱垘坿䝾邞琈豧玞畐㽬鶝鬴巿玸鳺䫍膚虙㐢㜑澓洑泭㳇㫙蝜蜅蚹䗄蚥哹踾䟔䟮嘸㕮咈罦輻畉䡍䍖輔輹㟊賦帗賻㠅岪翇㤱䪙韍㤔烰粰糐焤炥冨䘠袚褔衭襆複袝襥䃽禣祔鍢鈇頫負鰒鳧鮲鮒鮄鍑鳬鉜鉘䎅捬撫郙棴尃酜枎盙乶椨榑椱覄栿柎麬麩麱柫旉懯箙筟㓡䫝甶䠵䘀蛗峊鴔簠秿復稪艀䒇䒀䑧䵗彿笰乀竎㵗癁䦣㾈娐妋嬔婏媍婦䵾怤姇釡俛偩俌颫紱綒綍䋹䌿刜㪄縛䌗緮䋨絥弣紨紼諨訃㚆詂佛",
"ga": "嘎伽尬噶旮咖夹尕尜钆嘠錷釓魀玍",
"gai": "改该概盖钙溉芥丐垓赅戤陔葢蓋荄䏗瓂豥㕢䀭漑晐畡乢峐賅䪱忋祴鈣匃匄㧉摡槩槪㮣姟侅絠絯郂㱾賌該",
"gan": "感赶敢甘杆干肝乾柑竿赣尴苷秆橄坩擀绀酐泔玕灨旰矸澉淦疳䔈芉皯䃭尷尲趕幹榦倝迀鳱䲺攼尶盰澸漧㽏汵䵟骭䯎忓粓衦鳡鱤㺂魐檊桿䇞簳稈筸贑䤗贛凎仠凲紺詌",
"gang": "刚钢港纲岗杠缸冈扛肛戆罡筻犅牨矼堽堈䴚㽘㟵崗㟠剛岡焵焹釭䚗鎠鋼摃㧏掆槓㭎棡罁疘冮戅戇綱",
"gao": "高告搞稿膏糕羔镐篙睾皋诰槁藁锆杲缟槔郜菒䔌藳㚏夰䗣鼛櫜峼韟祮祰禞鋯鎬鷎㚖皐槹橰檺勂吿臯鷱筶㾸餻縞髙槀稾稁誥",
"ge": "个合各革格歌哥隔割葛阁戈胳颌鸽搁咯疙蛤骼铬膈嗝镉圪鬲硌盖哿塥虼袼搿舸䪂䩐鞈䕻戓㦴茖呄䧄牫騔㷴䐙肐䨣䘁䪺䫦臵鞷㵧滆滒䗘蛒㗆嗰轕輵㠷愅韚韐裓㝓䆟觡鎘亇饹鴚鮯鎶獦鉻犵匌挌㨰擱槅戨㢦櫊䈓㪾敋箇笴閣鴿䢔個佫佮彁諽䛋䛿謌",
"gei": "给給",
"gen": "根跟亘艮茛哏亙㫔揯搄㮓䫀",
"geng": "更耕耿庚梗哽埂羹赓颈鲠绠莄菮堩刯郠浭畊骾峺焿鹒賡鶊䱍䱎鯁䱭䱴挭椩㾘羮絚綆䌄緪縆䋁",
"gong": "工公共功供攻宫贡巩弓恭拱躬龚汞蚣珙肱红廾觥龷慐貢㔶䢼拲㭟䂬鞏䡗㧬㼦碽厷髸塨䢚㺬㫒唝嗊輁幊愩㤨熕宮觵匔匑栱㯯杛篢躳䇨㓋龏龔侊糼糿",
"gou": "构够句购狗沟勾钩拘苟垢篝枸媾佝诟笱岣鞲遘觏彀缑冓覯芶䃓豿撀㜌㝅㨌坸耇耉耈玽溝㳶蚼㗕啂㽛購䝭䞀韝煹㝤褠袧雊鈎鉤夠㺃搆構簼䑦痀姤緱訽詬",
"gu": "古故固顾姑骨鼓股谷孤估雇咕呱辜菇沽锢贾钴梏臌箍蛄汩蛊轱诂牯崮鸪鹘瞽痼鲴毂菰牿嘏罟觚酤巭薣盬㠬䓢蓇苽巬㠫夃㚉䜼䮩尳鴣㼋䀇脵皷鼔堌㯏䅽皼榖穀糓轂䍍䐨䶜䀦䵻䀰濲瀔淈泒蠱啒唃唂軲䡩䍛罛軱鶻崓愲祻鈷錮馉鮕鯝鈲䀜㧽扢橭棝榾柧杚箛稒笟篐㒴㽽凅㾶羖嫴傦餶逧僱䊺縎詁顧",
"gua": "挂瓜寡刮褂呱卦剐胍鸹括栝诖䒷劀騧趏坬颪啩踻叧罣冎剮歄㒷煱掛桰鴰䈑颳絓緺詿",
"guai": "怪拐乖䂯㽇罫恠叏夬㷇㧔柺枴箉䊽",
"guan": "关观管官惯馆贯冠灌罐棺斡倌纶矜盥莞鳏鹳掼涫䩪䪀鸛觀雚蒄覌礶瓘璭琯矔卝泴㴦潅丱䗆䗰躀輨䏓䎚悺慣爟㮡悹䙮䘾䙛窤祼鑵鳤鱹鱞鰥䲘錧鏆摜欟樌罆観筦䦎癏瘝痯関關闗舘館䌯遦貫毌䝺",
"guang": "光广逛胱犷潢咣桄茪黆炗垙珖洸㫛炚輄臦臩廣烡広灮炛銧獷姯僙俇",
"gui": "规贵归鬼桂轨柜硅龟跪瑰闺诡傀匮圭刽桧鲑癸皈炅鳜珪匦眭晷刿庋宄簋妫茥鞼匭蓕蘬㔳陒雟㸵騩䰎厬胿䝿㙺攰邽㪈郌䳏䞨垝昋鬹規槼嫢璝鬶椝瓌劌瞡瞆瞶䁛氿湀㲹蟡蛫螝貴䠩軌䯣䞈巂嶲恑庪廆袿䙆襘祪禬鑎䣀㩻觤亀鐀鱖鮭䲅鱥䤥猤摫撌㨳㧪櫃槻樻槶椢櫷檜筀歸龜䇈攱閨䍷䍯癐䐴嬀姽媯劊佹䌆詭帰",
"gun": "滚棍辊衮磙丨鲧绲蓘蔉䎾䃂㙥㯻睔滾䵪輥惃鯀鮌袞緄緷㫎䜇謴",
"guo": "国过果郭锅裹蝈埚帼聒虢椁腘粿掴蜾崞猓馘菓蔮聝䂸㞅䆐腂膕䐸堝墎㳀㶁淉漍濄蟈褁㖪㕵嘓啯㗻國囯輠囻囶圀幗過惈慖䙨鈛鍋鐹馃㚍懖摑楇䴹槨簂瘑䤋䬎餜彉綶彍涡",
"ha": "哈蛤虾铪鉿紦",
"hai": "还海孩害嗨亥骇咳氦嗐骸胲醢㜾駴駭㦟塰咍䯐㤥烸䱺㺔㨟㧡酼䠽䠹䇋妎饚餀",
"han": "含汉喊寒汗旱韩函涵罕憾焊憨翰撼邯悍捍酣瀚鼾蚶颔晗菡犴旰顸焓厂邗撖䕿䓿㽉䓍蔊莟顄凾圅馯駻厈䫲丆䏷䶃䐄爳䨡䖔㙳頇㙈垾韓㲦螒鶾䮧雗㙔䎯䧲琀䁔睅甝㵄漢涆澏浫㵎浛暵蜬虷㪋晘蜭蛿㘕㖤哻㘚㘎唅輚䍐崡嵅屽䍑㟏㟔熯㶰㸁䗙䘶䤴䥁釬銲魽鋎猂㺖鋡㨔扞皔㮀梒䈄馠筨兯閈闬㽳嫨㜦娢傼佄㒈㑵谽豃頷㼨䌍㢨䛞譀",
"hang": "行航杭巷夯沆吭绗颃苀垳䀪蚢䣈䟘貥㤚裄䴂魧筕笐䘕䦳絎斻頏迒䲳",
"hao": "好号毫耗豪浩郝壕嚎皓镐蒿嗥濠昊貉薅颢灏蚝嚆薃䒵茠薧聕䧚䧫䝞毜㬶䝥㘪淏㵆灝澔滈昦㬔暤暭晧曍䯫顥暠蠔㙱䪽號㕺噑哠嘷㞻㠙乚悎鰝獆獔獋皞皡皥皜㩝椃秏籇竓恏㚪侴䬉䜰傐儫㝀䚽鄗譹皋",
"he": "和合何河呵核喝荷吓贺赫盒颌褐鹤禾嗬壑诃涸阂阖劾貉龢翮菏盖盍曷纥蠚鞨䕣萂䒩䓼㹇䃒碋礉盇賀䶅貈䞦䚂㷤靏靎垎靍鸖齕㕡龁澕渮㵑䳚㬞螛毼㔠鹖㓭䫘鶡㕰嚇啝咊㗿哬嗃䵱䢗峆䳽㥺䪚㦦翯煂熆爀焃㷎籺粭熇燺袔寉鶴鑉釛鲄饸魺狢鉌皬㿣抲㭱㪃㰤㮫楁覈柇㭘㮝麧䴳篕䎋惒盉䅂闔癋閤閡姀郃敆頜㪉欱餄紇鶮訶訸詥謞苛",
"hei": "黑嘿嗨潶黒",
"hen": "很恨狠痕鞎䓳拫㯊佷詪",
"heng": "衡横恒哼亨珩鸻蘅桁㔰䒛胻脝㶇涥啈䯒恆悙烆䄓鑅撗橫鴴鵆姮䬖䬝",
"hong": "红洪宏轰鸿哄虹烘弘泓竑訇讧闳薨蕻荭黉鞃䩑葓䲨葒苰䧆耾硔翃䫺硡䃔䂫㬴黌垬霟霐䞑䨎玒沗玜䀧鬨澒鴻汯渱潂浤渹晎叿吰呍嚝㖓䍔䡌軣轟輷䡏屸羾灴䉺㶹粠焢翝䆖宖銾鉷鈜魟鋐鍧撔揈篊閧闀閎䪦竤闂妅娂仜䫹谾䜫谹谼紅紘纮㢬彋綋紭訌",
"hou": "后候厚猴侯喉吼逅篌齁骺堠鲎糇後瘊茩葔䂉㸸㕈鱟䞧豞睺洉㫗㬋䗔㗋㖃吽帿翵㤧翭䙈矦鲘䪷鮜鯸䳧銗犼㺅鍭郈垕㮢鄇䫛餱",
"hu": "互乎护呼户忽胡湖虎糊弧狐壶沪蝴葫瑚浒惚唬扈琥瓠囫鹄唿斛祜滹鄠鹕醐猢和许核觳虍轷岵怙煳烀鹱槲笏冱戽䩴芐萀㸦蔛匢匫䔯苸蔰䕶㕆鬍鶘鶦䭌綔瓳㪶䎁怘䮸膴䞱豰壺嗀縠㺉螜壷垀雽䨥䨼戸䁫虖歑虝雐鍙瀫沍淴汻䲵泘滬滸䗂昒昈㗅䠒嘑嘝嚛喖䍓軤幠恗䪝䊀䉿焀熩粐㝬寣隺鍸䚛鳠錿鱯鸌鰗魱鯱曶㫚㹱乕摢抇搰㿥䰧㨭楜㯛枑槴箶衚頶鵠䧼䇘戶䈸䉉乯簄㾰頀媩嫮嫭婟俿䬍餬䭍䭅弖絗護謼帍鳸㦿䛎戏",
"hua": "化话花划画华滑哗桦猾铧骅砉華鷨蕐黊蘤㭉䔢蒊驊硴夻磆䏦埖㓰䶤澅螖嘩㕦䠉㕷㕲呚㠏崋㟆㦊㦎糀鏵錵觟釫釪鋘䱻㚌撶摦搳㩇樺椛槬㮯枠杹䅿舙嬅婲畵畫劃婳姡嫿繣譁誮諣諙䛡話譮豁",
"huai": "坏怀淮槐徊踝蘾蘹䃶壊耲壞䴜瀤咶㠢懐懷櫰䈭㜳褱褢",
"huan": "还环换欢缓患幻唤焕寰桓痪宦涣豢獾浣奂洹圜鬟鹮垸萑漶逭锾鲩擐缳荁萈酄歡藧㿪㕕驩䭴䮝㹖貛䝠貆肒堚豲瓛環瑍雈睆䀨䀓澣澴㶎㵹渙㬊㬇㼫嚾喛喚還轘嵈䯘峘鴅懽㦥愌㡲糫煥䴟鵍寏䆠鍰䥧鐶镮奐烉鰀鯶鯇獂狟犿攌換梙槵㣪䈠歓䍺闤阛羦䦡瘓㓉孉嬛緩絙繯綄讙㪱",
"huang": "黄皇荒慌晃煌惶簧谎恍蝗磺凰隍幌徨潢璜湟肓篁蟥遑鳇癀䪄黃鷬葟㞷䮲騜奛䐵㬻䐠䑟墴塃趪䞹堭瑝䁜兤滉曂晄喤㡆崲䍿愰怳㤺熿䊣熀炾䊗宺鐄鎤鱑鰉鍠锽獚皝皩䳨㿠㨪揘榥櫎楻穔䅣艎韹㾠㾮媓偟餭䌙縨謊朚巟㠵衁諻詤",
"hui": "会回挥灰汇绘恢辉毁慧惠悔溃徽讳卉秽贿晦诙彗晖蛔桧诲喙洄荟珲蕙烩茴睢迴麾咴隳恚虺蟪缋蘳蔧薉匯㰥䕇藱薈隓䜐䧥芔䃣㥣靧䩈㩓毀毇䏨噕璤恵豗㱱㻅璯睳顪翽瞺頮颒滙湏洃泋潓輝濊瀈蛕㬩暳蚘蜖暉嚖嘒噅䫭囬廽逥圚廻㞧屷賄囘翙屶懳㤬憓恛翚翬烠烣燬㷐㷄煇燴寭袆䙡䙌褘禈鏸鐬䤧灳鮰獩㨤㩨㨹拻撝揮櫘槥檓橞檅楎篲䂕穢鰴幑䇻䅏徻闠阓痐瘣㜇彚媈嬒婎㒑僡會㑹佪儶餯㑰繢彙絵繪譿詼譭䛼譓䜋䛛諱詯誨堕",
"hun": "婚混昏魂浑棍荤馄珲诨溷阍葷蔒䧰鼲䰟琿殙睴睧尡渾涽䫟圂慁轋䡣昬睯忶㥵惛焝觨䚠掍㨡棔䴷䅙䅱閽婫倱俒㑮餛䛰諢",
"huo": "和活或火获货伙惑霍祸豁夥蠖嚯镬藿劐耠灬钬锪攉㦯韄䰥蒦騞奯剨臛耯靃眓矆矐䂄䁨濩湱瀖沎漷曤嚄嚿喐咟吙㗲㘞䯏旤雘㦜邩㸌煷窢䄀禍䄑䄆鑊䱛鈥鍃獲掝擭捇㨯檴䣶㯉穫秮䉟秳艧秴癨䦚閄彠彟佸俰貨䋭謋",
"ji": "机几基己期济及级计即极技记集际积纪急激既继击奇季鸡迹剂辑绩吉寄疾挤肌籍祭寂脊饥忌冀藉稽畸棘鲫叽圾嫉姬讥妓汲系伎缉唧骥羁髻悸瘠箕暨矶麂岌蓟亟戟跻诘犄荠稷畿霁嵇嵴屐蒺觊笈玑楫偈鱀勣芨咭其齐芰蕺剞赍殛乩洎虮戢跽哜墼鲚掎笄彐佶齑䓫䩯蘎鞿蘻蘮葪薊茤旡蕀蔇虀薺䓽焏際隮㤂䲯﨤㹄䯂驥䮺鳮䰏㞆㚡朞卙䦇惎諅磼磯䐀鶏膌䐕䐚鷄雞叝䨖趌䟌䞘䟇塉郆霵賷坖䣢耤耭垍賫㙫㙨霽㒫䢋㱞㻷㻑璣璾䶩茍㦸䁒㭰㲺㴕㴉湒濈瀱漃㳵泲鹡鶺漈潗済濟䗁螏蝍暩蟣嗘踖躤踑蹟蹐䠏躋跡㘍㗊㖢喞㗱嘰嚌羇羈轚擊檕罽輯毄㚻繋撃䍤䝸覬㡇䶓嶯㠖㞦㠍㥛忣㠱㥍丮鵋㞛愱懻妀庴廭㸄㲅襀襋禝禨錤觙觭銈銡鱾䤠鍓魥鰿魝魢鯚鯽鰶鱭鑙犱鏶鐖鑇㔕撠刏鬾魕㰟裚揤曁旣皀卽皍擠㨈鸄覉覊極㮟樭橶枅䤒檝㮨梞槣槉楖㭲檵機櫅䇫彶䚐嵆徛簊稘筓積臮箿稩躸䪢刉艥䒁鷑穊穄穖穧兾㾊痵癪㽺㾒㾵癠塈堲䳭姞䢳伋亼偮㑧飢饑谻㞃僟亽雧級綨績緁緝紀彑䋟継紒㡮幾㡭繼計韲齏剤劑齎齌㧀記誋譤譏䜞给",
"jia": "家加价假架甲夹佳嫁驾嘉贾钾稼颊伽挟迦枷荚戛拮浃胛袈痂颉镓岬笳珈蛱跏瘕袷葭恝郏铗莢䩡䕛斚犌戞㕅郟夾頰鵊㼪脥駕毠乫㔖鴐腵貑鴶㪴耞圿豭玾頬䁍䀹䀫浹泇蛺䖬唊斝䑝幏叚忦糘麚䴥裌鋏鉫鉀鎵猳拁抸扴㮖榎梜賈椵榢槚檟徦㿓婽傢價䛟",
"jian": "间见建件坚简渐减检践健尖监艰键肩兼鉴浅箭碱剪剑舰奸歼俭拣荐贱茧柬捡煎溅涧谏睑堑腱毽笺缄饯硷翦犍謇鲣僭锏缣囝鞯菅蒹戋戬湔趼踺蹇裥搛枧楗笕鹣牮谫戔韉靬韀鞬堅䵖㔋監鋻鍳鑒㯺譼虃囏艱蔪繭薦藆蕑蕳葌菺䧖䮿礷碊礛鬋䶠䩆礀磵礆堿麉䶬趝墹䵤鳽雃戩臶幵瑊珔䵡豜豣殱殲瑐蠒玪鹸鹻鹼見瞷睷瞼㓺瀳減洊瀐䤔漸濺瀽㶕澗湕㳨瀸暕鵑踐䟰跈轞䟅䭕賤䯛䯡賎帴㦗惤熞熸糋寋弿襺袸襉襇鑑鑬鳒鏩鰹鰔鰜鰎鑳㺝猏鐗鐧䥜鍵鐱鑯㨴挸揀擶揃㨵撿樫檻椷栫榗梘㰄椾検檢櫼箋㣤㔓䄯牋筧䅐馢籛䇟篯艦簡䉍徤䵛覵間覸冿鶼姧姦俴剣劍劎剱劒劔餞䬻䭠餰䭈㦰倹儉緘絸繝彅縑諓䛳譛鵳諫譾謭旔詃槛",
"jiang": "将讲江降奖蒋港匠疆浆姜僵酱桨缰绛犟强茳礓耩豇洚糨匞韁薑顜葁蔣䕬㹔膙塂壃䞪䙹畺殭䁰滰疅畕嵹翞糡鳉鱂摪摾橿櫤㯍夅䉃䒂奨醤㢡奬獎醬漿螀螿槳將傋䋌䥒繮勥謽絳弜弶講",
"jiao": "教叫较交觉角脚焦胶郊缴骄娇轿搅浇嚼校剿礁椒矫狡绞蕉酵窖饺跤佼侥皎蛟茭醮姣铰湫鲛峤艽噍挢敫徼僬鹪茮斠藠驕膠腳膲趭璬珓䂃䣤䴛䁶㳅灚澆漖䀊滘潐㬭曒蟜暞晈蟭䠛踋劋嘂嘄噭呌嘦轇轎較嶠㠐峧賋嶕嶣䪒憿憍煍烄燋䘨䆗窌䚩鱎鮫䥞獥鉸鐎㩰敎皭攪撹皦撟捁挍摷㰾譥釂㭂敽鷮敿矯徺臫笅穚簥筊㽲㽱虠䢒䴔鵁勦嬓嬌孂㚣僥龣儌餃鷦燞繳纐絞訆譑䜈",
"jie": "结解接阶界价节介姐借街揭届洁杰截皆戒捷竭劫桔藉诫秸睫楷芥婕拮孑诘疥嗟颉疖桀碣羯讦偈蚧毑袷家她卩喈骱鲒䕙鞊鞂蓵䔿菨莭㔾階卪岊犗礍䂝䯰䂶㛃镼砎䃈脻丯刦刧刼頡㔛劼㓤迼堺堦䣠琾疌玠䀷䁓潔尐滐蠽湝昅蛶蠘蜐蛣䗻蝔唶踕跲喼吤畍嶻崨幯㠹巀嵥岕悈屆㞯㦢㸅庎煯㝌衱袺褯衸㝏䥛觧鉣㘶鍻鎅鮚䰺䱄䲙魪狤擮㨗掲擑㨩掶搩杢㮮楬楐檞桝榤㮞椄徣䂒䅥節蠞稭㓗㾏㿍楶癤痎䇒媎媫嫅媘㑘倢偼䲸傑飷結䌖鶛誡訐詰誱謯䛺",
"jin": "进金今近仅紧尽禁劲津斤晋锦浸筋巾谨襟靳矜瑾烬噤缙觐馑堇荩卺赆廑衿钅槿妗蓳荕菫緊覲㝻歏黅藎䒺巹㹏矝厪㰹砛䐶墐壗晉㬜琎瑨殣琻勁珒璶璡齽䶖鹶漌溍浕濅堻濜㴆㬐䗯唫嚍䝲贐惍㶦煡燼寖䘳䆮祲觔釿錦釒㨷劤搢䖐䤐枃䫴㱈㯲㯸䑤凚嫤㶳盡䀆賮嬧僅仐侭伒僸饉䭙儘進縉䋮䌝紟謹䥆",
"jing": "经精境京静竟惊景睛镜径警晶劲竞净敬井颈茎鲸荆靖兢痉憬泾菁粳阱胫腈迳旌璟儆箐刭肼靓獍婧弪荊莖葏㢣蟼憼驚䔔聙頚㣏㕋脛鼱㘫坓汬丼璥靜靚䴖鶄殌璄巠剄頸鵛逕坙梷淨汫瀞㵾涇澋浄曔暻㬌踁䵞䡖幜麠麖宑穽鯨㹵猄鏡坕桱橸稉徑秔凈痙竸競竫竧妌婙婛俓傹経弳經綡䜘鶁亰旍誩",
"jiong": "炯窘迥炅颎冂扃蘏蘔褧駫駉澃䐃坰埛㷡煛泂浻煚㖥囧冋㢠冏䢛燛㤯烱逈㷗㓏㑋僒侰絅䌹綗熲顈",
"jiu": "就究九久旧酒救纠舅揪灸疚臼鸠厩赳韭咎桕啾柩鹫鬏玖阄僦匶萛韮匛䓘舊牞镹䊆䳔䳎慦㺩㺵殧齨䰗鬮㲃汣䡂㠇丩乆䊘㡱廏廐廄㶭麔䆒鯦勼匓捄摎㧕揂㩆欍柾朻樛杦舏䅢揫㐇鳩奺倃糾乣糺紤鷲䛮",
"ju": "具据局举剧句居巨距聚拒柜菊矩惧俱拘桔咀锯鞠橘踞驹沮瞿炬踽疽遽掬枸飓榘苣裾龃榉倨狙钜莒且车苴鞫犋雎琚屦窭锔醵椐讵蘜䕮䢹乬巪蒟輂埾陱聥犑駏驧駶駒䃊砠㪺䢸舉㐦擧鴡貗腒䏱鼳鼰毩毱弆壉趜埧㘲耟㠪歫䶙齟䶥郹䴗鶪㮂狊䋰勮豦劇愳虡眗䡞洰㳥挙湨澽涺泦泃淗趄昛蚷㬬蜛䗇蹫跙㘌躆跼跔踘啹罝㽤巈岠岨崌㞫鵙怇鶋懅懼䪕㥌屨㞐凥烥粔焗粷寠袓襷䆽窶䄔鉅鐻邭鋸鋦鮔匊䱟鮈鵴䱡據㩴㩀㨿挶䰬抅㐝拠檋櫸欅䣰䤎椇梮椈秬簴筥躹䅓艍䈮䵕閰姖娵㜘婮婅倶侷颶䜯繘詎䛯諊渠",
"juan": "卷倦捐圈娟鹃绢眷涓镌蠲鄄狷锩桊蔨菤奆朘腃臇埍睊睠淃瓹呟罥羂䳪脧惓慻焆㷷裐隽鋑䥴獧錈鎸鐫捲䚈䣺㯞䅌䡓勌劵䄅龹䖭帣巻餋弮勬絭姢䌸㢧絹㢾讂㪻",
"jue": "决觉绝掘嚼爵诀厥倔攫崛蕨獗撅噘抉镢蹶谲角孓噱橛珏矍鳜桷钁劂爝觖匷㓸芵蕝孒䦼矡駃砄蹷蟨憠鷢橜䐘䏣臄貜䏐䁷覺趉䞵䞷赽瑴䝌玨㻕玦亅䀗覐㵐決覚泬灍蟩䖼蚗虳噊䟾躩䠇趹爴䡈㟲嶡嶥崫㤜憰戄屩屫刔鴂爑㷾熦焳䙠䘿䆕䆢氒鐍鐝觼觮䦆鈌鴃玃㹟㩱挗㸕捔撧㰐㭾㭈櫭䍊䇶欮疦瘚弡彏䋉㔢絶㔃絕譎斍訣",
"jun": "军均菌君俊峻钧郡骏竣隽浚筠麇儁皲捃莙葰䕑陖皹駿鵕㕙碅㓴埈䝍㻒珺䜭濬汮㴫晙蜠蚐呁㽙畯賐懏燇麏麕皸軍袀㝦寯鲪銞馂鵔鮶鍕銁鈞攈攟棞桾箟箘䇹姰頵鵘覠㒞餕㑺雋龟",
"ka": "卡咔咖咯喀佧胩垰裃鉲䘔",
"kai": "开凯慨恺揩楷铠忾闿锴岂蒈垲剀锎䒓奒䐩塏䁗暟嘅䡷輆剴颽凱㡁嵦愷愾炌烗鎧㚊鎎鐦鍇開闓勓欬",
"kan": "看刊堪砍坎勘嵌侃槛瞰龛阚磡戡莰凵顑歁墈栞䶫鬫矙轗輡嵁崁惂冚欿衎㸝䘓㸔䀍竷闞龕偘",
"kang": "抗康炕扛慷亢糠鱇伉钪闶匟砊漮䡉囥嵻忼㱂粇㝩鏮犺鈧槺躿穅閌嫝邟㰠",
"kao": "考靠烤铐拷犒尻栲䐧攷丂洘䯌嵪㸆銬鲓鮳鯌䯪髛",
"ke": "可科克客刻课颗壳棵渴咳柯磕苛坷瞌窠蝌轲颏恪稞髁珂氪缂岢嗑剋尅呵骒溘蚵锞钶疴薖萪匼騍牱犐礚碦勊勀砢㕉堁殼殻㵣渇顆敤㪙趷礊軻嶱嵑㞹嵙峇愘炣㪡愙䙐錁翗鈳搕揢榼醘㐓㪼㤩衉艐痾㾧牁娔樖緙課頦",
"kei": "剋尅",
"ken": "肯恳垦啃龈裉㸧硍墾懇貇豤肻肎褃錹掯",
"keng": "坑吭铿硻阬牼硁硜䡰鏗鍞銵摼挳妔誙劥",
"kong": "空控孔恐箜倥崆鞚硿埪涳㤟悾鵼錓躻㸜",
"kou": "口扣寇叩抠佝蔻芤眍筘剾蔲瞉鷇㲄瞘滱䳟怐冦宼㓂窛釦敂䳹摳劶㔚簆彄",
"ku": "苦哭库枯裤酷窟挎骷绔袴刳堀喾䧊郀矻嚳㱠跍圐㠸庫廤㐣焅褲鮬狜楛桍䇢秙䵈瘔㒂俈絝",
"kua": "跨夸垮挎胯侉咵趶骻䯞銙舿姱誇䋀",
"kuai": "会快块筷脍侩狯哙蒯浍郐䓒巜膾凷墤㙕㙗塊圦㱮欳澮㬮噲䯤㟴廥糩鲙鱠獪擓㧟㔞䈛鄶䭝儈旝",
"kuan": "款宽髋䕀臗髖寛寬窾窽䥗䲌鑧䤭㯘歀梡欵",
"kuang": "况矿狂框旷筐眶匡邝哐圹诳劻夼贶贶纩诓匩邼硄礦砿壙眖矌洭黋況曠昿軭軖軦軠岲貺恇忹懭鄺懬爌䊯鋛鑛鉱㤮鵟狅抂䵃筺穬儣絖纊絋誆誑",
"kui": "亏溃愧奎魁馈葵窥盔傀匮逵夔喟睽喹聩揆篑岿馗蒉蝰暌跬悝愦䕚蘷藈匱蕢䕫虁聵聭聧骙騤犪尯磈㚝膭頍㙓刲䖯殨㕟虧潰晆䠑䟸躨蹞嘳顝䯓巋巙憒煃窺頯鍷鍨㨒䫥楏䤆櫆楑籄簣䈐䦱闚䍪㛻嬇媿戣鄈䳫饋餽䧶謉",
"kun": "困昆捆坤锟崑鲲琨髡堃醌悃阃菎騉髨髠硱堒壼壸瑻睏涃潉蜫䖵晜㫻鹍鵾䠅崐焜熴鶤裩裍裈褌祵錕鯤猑㩲梱稇稛閸閫綑",
"kuo": "括扩阔廓蛞鞟鞹萿葀䯺髺鬠霩濶䟯㗥韕挄擴拡頢筈䦢闊",
"la": "拉啦腊辣蜡落喇垃剌旯邋砬瘌藞鞡䪉菈䏀鬎磖䂰㕇䃳臈臘䟑䝓䶛㻋㻝瓎溂䗶蝋蝲蠟嚹翋㸊爉鯻鑞镴搚揦攋䱫揧辢楋櫴柆䓥",
"lai": "来赖莱癞睐籁徕涞崃疠唻赉濑铼䓶藾萊䧒騋㚓䂾琜睞瀨瀬淶䠭㠣崍庲襰䄤䲚鯠錸猍梾頼賴鵣棶郲來賚顂鶆逨䚅麳筙㥎籟徠箂䅘癩㾢婡俫倈䋱",
"lan": "兰蓝烂览篮栏拦懒滥揽澜婪岚缆阑榄斓褴啉谰镧漤罱藍韊䪍覧覽擥蘫蘭葻䰐䃹䑌壈璼㱫瓓灆濫灠灡浨㳕瀾嚂囒躝㘓幱嵐㞩懢懶惏㦨爁爦爤糷䊖顲燗爛燷燣襤襽襕襴䆾钄䳿鑭㩜攬㨫攔欖㰖欗醂欄籃籣䦨闌㜮孏嬾㛦孄儖㑣㑑繿纜䌫䍀譋斕讕",
"lang": "浪朗郎狼廊琅螂啷榔鎯莨阆蒗锒稂䕞蓈蓢硠朤朖㙟埌㱢瑯䁁䀶蜋㫰䍚䡙䯖崀㟍㢃烺䆡㝗䱶鋃樃桹躴艆筤㾿閬㾗嫏郞塱㮾勆郒欴㓪斏誏",
"lao": "老劳落牢络捞姥烙唠涝佬潦痨酪崂醪乐耢铹铑栳荖䵏䕩硓磱嗠䝤朥耮耂㐗䳓珯澇労浶蛯蟧㗦咾嘮哰轑㟙㟹嶗㟉㞠恅憦顟粩䃕勞憥䝁窂銠鮱鐒䲏狫㧯撈㨓橯䇭躼軂簩癆嫪僗髝䜎",
"le": "了乐勒肋仂嘞鳓泐叻艻阞砳㔹玏氻㖀忇㦡鰳鱳扐楽樂簕竻韷餎",
"lei": "类累雷泪勒蕾垒肋擂磊儡镭耒羸嘞檑酹嫘缧缧诔䒹蕌蘲虆藟蘽蔂蘱絫厽㹎䮑礌礧磥㲕䐯鼺䨓靁㙼䢮䣂頛㼍瓃矋㵢洡灅㶟涙淚㴃蠝䍥䍣塁罍礨㔣壘壨畾纍轠鸓䴎櫐㡞類頪纇颣禷鐳銇鑸鑘鱩錑攂㭩䣦欙櫑樏䉪䉂䉓癗㿔㒍㑍㒦儽傫纝縲䛶誄讄",
"leng": "冷愣楞棱塄薐䮚碐堎睖踜㘄唥䚏䉄稜倰䬋",
"li": "里理力利立例离历李礼丽粒隶哩璃励黎厉厘梨莉吏栗犁鲤狸砾沥荔篱漓笠蛎痢俐锂俚雳逦戾镉罹栎蠡俪藜鹂骊砺蜊黧娌莅猁疠傈唳溧疬慄醴砬喱鬲苈澧蓠坜嫠郦呖跞轹詈粝鲡鳢枥篥缡藶蒚蒞荲䔆䔁䔣䔧蔾菞䔉苙茘䓞蘺䧉犡䮥䮋驪勵厲礪㔏礰鬁㻎砅䃯礫歴暦厯磿歷厤曆㻺㽁貍䤚蠫䴄脷壢靂隷䟐赲䟏靋塛孷釐剺斄㹈瓑珕蟸叓䣓䰛酈鸝邐䚕婯麗䴡㱹㡂㽝瓅瑮琍瓈䶘㮚䁻睙濿瀝浬浰沴涖灕蠇䘈曞蠣蛠㬏蝷蚸蟍蜧㒿嚦㘑囇躒㗚唎嚟㕸囄轣䡃轢䍠䍦豊巁屴峛峢㟳峲㠟岦㤦㤡㦒悧悷䊪爄糲糎爏廲粴麜㷰裡褵䙰禲禮䄜䥶觻䲞鋰鱱鳨鱺鯉鱧鯏㺡鏫鑗鉝瓥㼖攊㿨攦㸚擽皪搮㧰攭櫔櫪栛朸隸䣫欐䤙醨栃檪櫟鷅梸㰀㯤欚棙樆㰚䅄穲䖽䵩悡鋫䱘㴝犂睝䖿鯬鵹䊍邌錅䴻棃剓筣䉫秝艃䵓䅻籬癘竰癧䍽㿛㾐㾖鴗凓䇐孋㓯娳刕儮儷䬅䬆㑦㒧劙䗍盠盭䰜纚䋥綟縭讈裏離謧",
"lia": "俩",
"lian": "联连脸练炼恋莲怜链廉帘敛镰鲢涟殓濂梿奁裢潋楝蔹臁琏琏蠊裣匲蓮薕萰蘞匳蘝聨聫聯䏈聮奩鬑䃛磏臉䨬覝堜鄻璉㱨殮瑓䁠㶌瀮漣湅濓溓瀲澰㶑螊蹥嗹噒連㦁㡘慩翴㦑憐䙺㥕燫煉劆㢘熑褳襝鏈鰱鰊鐮錬鍊㺦䥥鎌㼓摙櫣㪝槤㼑㰈㯬㟀簾䆂䇜籢籨亷㾾㝺羷㜕嫾嬚媡㜃㜻斂㪘歛㰸僆䭑縺練䌞纞謰戀",
"liang": "两量亮良粮梁俩凉辆谅粱踉晾靓莨墚魉椋䩫䓣駺㹁脼㔝兩両涼湸蜽唡啢䠃喨哴輌輛輬辌㒳䝶悢糧裲䭪鍄掚魎䣼樑倆倞俍緉諒",
"liao": "了料疗辽僚聊廖缭寥撩燎撂瞭缪嘹潦寮镣蓼獠尥鹩钌藔䒿镽䩍尞鷯遼䨅㶫膫㙩璙䝀敹漻㵳暸蟟曢蹽蹘䍡嶚嶛髎嵺賿憭憀屪鄝䢧䎆廫膋爎㡻䉼炓㝋窷竂釕鐐爒㺒橑䄦簝䑠療嫽尦飉豂䜮繚䜍",
"lie": "列烈裂猎劣咧冽趔鬣埒洌躐捩茢䓟聗㸹犣鬛㼲脟㲱埓劽䴕㤠烮鮤鴷迾姴䁽浖毟蛚㬯哷䟹䟩㽟煭鱲猟獵㧜挒挘擸栵㭞㯿䅀䉭巤颲儠䜲",
"lin": "林临邻磷淋鳞霖麟琳拎凛吝粼赁蔺躏嶙啉璘廪檩遴膦瞵辚辚懔臨䕲菻藺隣阾厸驎䮼䫰碄壣瀶潾澟暽䗲晽躪蹸躙㖁轥疄轔崊恡悋懍燐㷠䢯鄰粦㔂亃翷斴甐麐廩冧㝝䚬鱗鏻獜撛㨆橉䫐檁箖䉮焛閵癝凜癛僯賃繗綝㐭",
"ling": "领另令灵零龄岭铃玲凌陵棱菱伶苓聆翎绫羚鲮呤棂蛉囹瓴酃泠柃䔖蘦䖅蕶蔆蓤䕘䧙駖㸳砱朎霊霗㪮䰱龗霝䴒䚖孁靈㲆䨩夌坽䴇霛琌㱥㻏齡羐鹷齢澪淩㬡昤㖫跉䡼䡿輘軨䯍崚岺嶺㦭㥄爧燯炩㡵䴫麢䙥裬袊祾䄥錂鯪魿狑鈴掕皊櫺欞㯪醽䉁䍅䉹䈊䉖䠲舲彾秢笭衑竛閝㾉婈姈鸰刢領鴒䌢綾紷詅〇",
"liu": "流六留刘硫柳溜瘤碌榴馏琉浏绺蹓遛镠骝鎏鹨熘镏锍旒蓅藰蒥䋷䭷驑駵駠騮磟磂䶉㙀塯霤㽌璢畱鬸珋瑠䰘澑畄瀏瑬蟉䗜㽞嚠疁罶嵧羀懰鷚翏雡熮㶯廇麍裗䄂䚧鐂鏐䱞䱖鰡鎦鋶鹠劉鶹㨨橊桺栁桞橮䉧癅嬼媹飗飂䬟飀飅餾綹㐬斿旈",
"long": "龙隆笼垄拢胧聋咙陇窿珑垅弄砻茏栊滝眬泷癃䪊蘢䃧隴䏊龓尨礲朧霳䥢鏧壠靇瓏矓漋㙙㴳湰瀧昽曨蠬哢躘嚨嶐㟖巃巄贚㦕㢅爖㝫襱竉鑨攏梇䙪櫳槞㚅䡁徿籠䆍篭聾礱龍壟龒蠪驡鸗㰍竜㛞㑝儱豅㡣",
"lou": "露楼漏陋搂喽篓娄镂偻髅蝼瘘耧蒌嵝鞻㔷蔞䮫㲎塿耬䝏剅瞜䁖漊溇螻嘍䣚䫫婁甊遱鷜㪹髏㟺嶁屚慺㥪廔熡䄛鏤䱾㺏摟樓簍䅹軁艛瘻瘺謱",
"lu": "路陆绿露录鲁炉卢芦鹿碌禄卤虏庐噜麓颅漉辘掳六赂鹭戮泸橹璐潞鲈撸蓼箓轳胪垆氇鸬渌辂镥栌簏舻逯虂䩮蘆蓾蕗蔍菉陸䎼騼䮉騄馿䰕磠硵䃙硉臚膔氌䐂壚塷趢塶圥勎坴鵱瓐㱺璷琭矑虜㪭盧顱鸕鹵睩淕瀘滷澛瀂淥曥蠦螰㫽踛嚧蹗鷺䟿嚕㖨黸䡜轤轆輅䡎髗㠠賂峍㟤㦇䎑勠剹㢚廬爐廘熝粶䴪㼾䘵祿錴鐪鑪鏀㔪鏴鯥䲐鱸魯鴼鵦䱚鏕魲鑥獹録錄鈩擄攎摝擼醁㯭櫨樐㯝樚櫓㯟㭔椂枦甪罏稑籚簬簵穋簶穞籙艣艫艪舮㓐㿖㛬㪖䚄盝㜙娽僇侓纑彔䌒㢳㪐謢玈",
"luan": "乱卵挛峦滦鸾孪栾銮脔娈䖂虊亂灤羉圞圝釠癴癵鵉孿㝈奱㡩灓曫巒鸞鑾攣欒孌臠㱍龻䜌",
"lun": "论轮伦仑沦纶抡囵崙菕芲陯磮碖腀耣埨淪溣蜦踚㖮圇輪崘惀㷍鯩錀㤻掄棆䑳稐䈁婨侖倫綸論",
"luo": "落罗逻洛络螺裸萝锣骆烙骡啰珞箩摞捋倮瘰猡硌荦脶漯泺镙椤雒蠃蘿蓏騾駱䯁硦覶頱腡㼈㱻覼䀩㴖濼曪囉囖邏羅峈㦬犖鏍鑼鮥玀㩡攞㰁欏洜㓢鵅籮躶䈷笿癳㿚㑩儸饠㒩纙絡䌱䌴驘臝䊨鸁䇔詻剆㽋咯",
"lv": "律率绿虑旅氯铝履吕捋驴滤侣屡缕榈褛偻闾稆膂藘葎䕡驢膢膟垏勴慮濾郘呂氀㠥嵂屢爈焒褸祣鑢鋁㲶捛挔櫖梠櫚穭箻閭儢侶僂絽縷緑綠繂膐",
"lve": "略掠锊寽㔀畧㨼圙鋢鋝稤",
"ma": "马吗妈麻嘛骂码抹玛蚂蟆犸嫲么杩蟇蔴䣕馬䣖遤碼鬕瑪睰溤螞䗫嗎駡嘜罵䯦犘㦄䳸祃禡鎷鰢鷌獁㨸榪㾺痲痳閁媽㜫㐷傌㑻摩",
"mai": "买卖麦脉埋迈霾荬劢唛薶勱邁蕒䮮脈霢霡䨪賣売䨫䁲嘪䚑鷶買麥衇䘑䈿㜥佅䜕",
"man": "满慢漫曼蛮瞒蔓馒螨幔缦鳗谩颟墁埋鞔熳镘䕕顢㒼蔄蘰鬗䯶鬘䰋䐽䝡䝢㙢䟂瞞満滿㵘澷蟎鄤㬅㗈㗄䡬㡢慲屘悗䊡襔鏋鏝鰻獌摱樠槾䅼䑱姏娨嫚㛧僈饅䜱縵謾䛲矕蠻",
"mang": "忙盲茫芒氓莽蟒铓牤邙硭漭䒎莾蘉茻牻駹厖硥壾㙁㻊䁳䀮盳浝汒蠎㬒蛖哤䟥䵨㟿㟐㟌㡛恾庬㝑鋩狵釯杧䅒笀䈍痝娏䖟杗吂",
"mao": "毛矛貌冒贸帽猫茂茅髦瑁锚牦铆卯懋袤昴峁眊茆瞀蟊蝥耄泖旄蓩鶜䓮芼鄚萺堥暓䖥愗髳冇貓䫉覒氂犛㲠㺺渵㴘冐毷㪞㒻㫯蝐罞軞䡚冃㡌戼㝟錨夘鉾䀤鉚乮鄮貿㧌㿞㧇皃㒵楙柕㮘枆酕䅦笷媢㚹䋃",
"me": "么濹嚰嚒",
"mei": "没美每妹梅煤眉霉媒枚酶镁媚魅玫昧莓糜楣寐湄嵋袂浼鹛镅猸䒽葿䓺苺脄腜脢堳坆㺳䜸瑂珻眛睸䀛湈沬沒渼䰪蝞跊嚜槑䵢黣䍙嵄郿鶥韎㶬䊊煝塺䊈燘禖祙鎇鋂鎂抺攗鬽挴楳㭑䤂栂䆀䰨躾黴徾篃毎䉋羙凂痗媺嬍媄睂旀",
"men": "们门闷瞒懑扪汶焖钔虋菛璊玧㱪懣㵍暪㡈䝧㥃㦖䊟穈燜䫒鍆㨺捫椚門悶閅們",
"meng": "梦蒙猛盟孟萌朦氓锰懵蟒勐檬濛蜢虻蠓矇瞢甍礞艨艋䓝鄸䒐䠢顭夢莔氋鹲鸏蕄䰒㚞䑅䑃䏵㙹靀霿霥矒溕曚䗈甿㠓幪懜懞冡鼆䀄䙩㝱䙦錳䴌䲛鯭鯍䥂獴䥰㩚掹擝橗䤓䴿䵆䉚㒱癦䇇㜴儚饛鄳夣蝱",
"mi": "密米秘迷蜜弥泌眯咪觅谜靡糜猕谧醚嘧弭脒幂麋縻汨蘼蘼芈敉宓冖祢糸蔝㰽蒾䕷蘪藌蔤葞䕳䮭镾覔㫘䪾覓㸓塓鸍羋瞇䖑濗漞濔㵋㳴㴵灖洣滵淧沵沕䌘渳瀰㳽羃䍘峚幎㠧㟜怽幦戂㥝㐘粎䊳麊熐麿爢㸏麛䴢冪宻鼏䁇冞㝥袮禰祕䱊銤獼㩢覛擟攠㨠䤍䤉釄醿醾䣾榓櫁樒簚䉾㜆孊侎䭩䭧䌩䌐㣆䥸彌㜷瓕䌕䋛䌏䛉謐䛑䛧謎詸",
"mian": "面免棉眠绵勉缅腼冕娩沔湎眄渑宀芇葂䏃䰓勔靦靣䃇㻰㤁丏麺䀎睌矈矏矊汅㴐澠蝒㬆喕愐糆㝰鮸緜㮌䤄杣㰃櫋麵麪麫檰䫵臱媔㛯婂嬵偭㒙緬絻綿",
"miao": "描苗妙秒庙渺瞄缪淼藐缈邈鹋眇喵杪鶓㦝䁧䖢㠺庿廟劰篎䅺竗媌嫹㑤緢緲玅",
"mie": "灭蔑篾咩乜蠛薎孭礣烕䩏䁾瀎滅䘊哶吀幭懱鴓鑖鱴搣櫗衊䈼㒝",
"min": "民敏闽皿悯抿泯岷闵苠珉玟黾愍鳘缗蠠䃉䂥碈砇垊琝瑉琘䁕盿湣潣旻旼䟨䡅罠䡑䡻㟭崏㞶䪸敯刡㥸鴖暋㟩敃惽怋憫忟鍲鈱䲄錉㨉捪笽笢簢勄慜鰵閩冺痻閔姄僶緡㢯䋋黽緍忞",
"ming": "明命名鸣铭冥螟茗瞑酩溟暝蓂眀眳洺㫥鳴朙㟰慏䊅鄍䒌䫤覭㝠䆩䆨䄙銘猽掵榠凕嫇姳佲詺",
"miu": "谬缪謬",
"mo": "么没模末默莫摸脉磨冒膜摩墨漠魔抹沫陌寞摹蓦蟆蘑馍谟茉貉秣殁貘万貊耱麽镆瘼嫫嬷嬷靺䒬莈驀㱳謩藦䮬砞䩋礳䃺䏞貃䳮塻圽歿歾瞙眜瞐䁼眽眿尛蛨黙昩䘃蟔嗼嚤䁿㱄髍帞帓懡糢㷬爅㷵䯢劘麼䜆庅鏌銆魹䱅魩獏㹮皌擵枺橅䴲䉑妺嫼嬤饃䬴饝纆絈謨嘿",
"mou": "某谋牟眸缪呣哞鍪蛑侔䥐劺鴾䏬䗋踎䍒恈䱕㭌麰繆謀",
"mu": "目母木模莫幕牧亩墓姆慕穆暮姥牡拇睦募沐牟缪苜钼毪坶仫莯䧔鞪䱯楘㜈牳砪氁胟雮霂畞䀲暯蚞踇畂畮峔幙慔毣炑䥈鉬狇鉧㣎㧅䑵艒㾇凩㒇縸䊾畆畝畒",
"na": "那哪拿纳娜呐捺衲钠内南肭镎靹蒳䖓乸䫱貀豽䏧雫䀑㴸䖧䟜吶㗙嗱軜䎎䪏袦鈉魶䱹鎿㨥䅞笝䇱郍䇣䈫拏妠搻納䛔",
"nai": "奶耐乃奈萘氖迺艿能鼐柰孻螚䘅䯮腉渿褦釢錼㮈㲡廼㮏疓㾍䍲嬭倷",
"nan": "难南男喃楠囡赧囝腩蝻䕼䔜萳戁難莮䔳遖䁪湳暔㫱㽖畘䶲煵揇抩枏柟䈒㓓婻娚侽諵䛁",
"nang": "囊囔曩馕攮䂇嚢灢㶞蠰乪擃欜齉儾㒄饢",
"nao": "脑闹恼挠瑙呶孬桡淖铙硇垴蛲猱夒䃩碙碯臑脳腦䑋䴃堖鬧蟯巎嶩悩憹怓惱鐃獶獿峱㺀㺁撓䄩閙嫐㞪㛴䫸㑎匘譊䛝詉䜀䜧",
"ne": "呢呐讷哪疒䭆䎪眲㕯抐訥",
"nei": "那内哪馁脮腇㼏㘨㖏䡾內䳖鮾䲎鯘錗㨅氞氝娞㐻餒",
"nen": "嫩恁㶧㯎㜛嫰",
"neng": "能䏻㲌㴰",
"ni": "你疑尼泥拟逆妮腻倪匿溺霓昵睨怩鲵铌旎呢坭猊伲䘌臡苨䕥薿孴聣隬䧇膩貎胒䝚郳㪒堄䁥齯惄眤㵫淣聻埿氼暱晲蜺蚭跜輗㞾㠜㥾㦐愵籾麑䘽䘦觬鈮鯢狔㹸掜屔抳䰯擬棿檷柅䭲馜秜䵒䵑屰䦵嫟嬺婗妳儞㲻伱儗㣇縌誽䛏",
"nian": "年念粘碾撵捻辗蔫拈埝黏鲶鲇辇廿䩞卄輦涊㲽淰躎蹍蹨哖唸㘝㞋惗焾鮎鯰攆撚䚓鵇秥簐䄭秊艌䄹姩䬯",
"niang": "娘酿䖆醸釀嬢孃",
"niao": "鸟尿溺袅脲茑嬲蔦䮍䦊䃵䐁㳮㠡㞙鳥䙚裊㭤樢嬝嫋㜵㒟褭",
"nie": "捏聂涅孽镍蹑蘖镊颞啮嗫摄乜陧臬糵㜸苶菍聶顳隉孼蠥糱櫱䯅䯀䯵齧㚔䂼㘿㙞摰槷湼㴪圼囁囓躡踙嚙踂噛踗㡪嵲嶭巕㸎䄒鑷鑈钀鎳錜揑㩶枿㮆籋臲篞㖖痆闑帇敜䌜䜓讘捻",
"nin": "您恁脌囜㤛拰䋻䚾䛘",
"ning": "宁凝拧狞咛柠泞佞聍甯䔭薴聹鬡㿦矃澝濘䗿嚀寕㝕㲰寍寜鸋寧寗鑏獰擰橣檸㣷嬣儜倿䭢侫",
"niu": "牛扭纽钮拗妞忸狃靵莥䒜牜䏔㺲䀔汼炄鈕杻䋴紐",
"nong": "农弄浓脓哝侬蕽鬞膿䢉䁸濃噥農燶㶶襛禯㺜挵挊醲檂欁辳齈穠秾䵜癑儂繷譨",
"nu": "努奴怒弩帑孥驽胬搙䢞笯駑砮㐐傉伮㚢",
"nuan": "暖䎡渜㬉煗煖䙇奻餪",
"nuo": "诺娜挪糯懦喏傩搦难锘逽㔮蹃㡅愞懧糥糑鍩䚥掿㰙梛榒橠稬穤㐡㛂儺㑚諾",
"nv": "女衄恧钕朒沑籹釹衂",
"nve": "虐疟硸䖋䖈瘧婩",
"o": "哦噢喔筽",
"ou": "偶呕鸥殴耦藕讴禺沤怄瓯区欧蕅毆鷗歐甌䚆鴎藲膒腢塸漚㼴嘔吘䯚慪熰鏂䳼櫙㛏㒖䌔䌂謳",
"pa": "怕爬帕扒啪趴琶耙杷葩钯筢䔤苩䯲䶕潖帊袙皅掱舥妑",
"pai": "派排迫拍牌湃徘俳哌蒎犤沠渒㵺䖰輫鎃猅棑㭛簲箄簰",
"pan": "判盘胖潘盼叛攀畔拌蹒泮蟠磐槃爿袢柈番襻丬萠蒰聁䰉䰔磻䃑䃲坢眅㳪溿沜瀊洀蹣跘炍鑻鋬牉䈲鞶幋縏盤鎜搫媻頖鵥冸詊拚",
"pang": "旁胖庞乓磅螃彷滂徬耪逄䮾厐龎肨膖胮霶㫄雱䨦眫㤶㥬炐龐鳑鰟舽䅭㜊嫎䒍覫",
"pao": "跑炮泡抛袍刨咆疱狍庖匏脬鞄䩝萢皰礟礮靤砲奅褜垉㘐軳麅麃炰拋爮㯡麭䶌㚿䛌",
"pei": "配培陪佩胚赔沛妃裴呸帔辔霈锫醅旆蓜阫馷䪹䲹䫠肧毰珮㳈浿㫲䣙賠㟝怌㤄䊃犻錇㧩衃姵俖伂轡裵斾",
"pen": "盆喷湓葐翸歕喯噴呠瓫",
"peng": "朋碰棚蓬膨捧篷鹏烹砰澎抨怦硼嘭彭堋蟛莑芃蘕駍騯鬅髼鬔䰃磞硑鵬蟚塜塳㼞淎泙踫輣軯䡫剻㥊憉恲熢袶䄘鑝錋匉捀皏掽樥槰椪䴶梈椖稝竼篣閛韸韼㛔倗傰纄弸苹亨",
"pi": "皮批屁披辟疲脾匹劈僻副罢譬啤琵坯癖毗痞枇霹噼裨媲否貔丕吡陂砒邳铍圮睥蜱疋鼙陴埤淠蚍罴甓庀擗郫仳纰苉鴄㓟隦阰駓髬㔻礔磇䏘豾脴腗䑀䑄膍肶豼噽嚭壀耚疈錃潎澼蚾蚽䠘㔥羆䡟毘岯嶏崥翍礕䴙憵鷿鸊悂炋焷螷蠯鈹銔鉟銢鲏鮍魾魮䤨釽錍狓狉鈚抷㨽揊䰦䫌䤏㯅秛秠稫篺笓鵧㿙闢嫓伾伓枈紕諀旇䚰䚹",
"pian": "片偏篇骗扁翩骈胼蹁便犏谝貵䮁騈駢騙腁䏒跰囨骿賆魸鍂楩楄覑㸤㾫㛹媥㓲騗鶣㼐諞",
"piao": "票飘漂瓢瞟缥剽嫖朴嘌骠慓殍螵薸䕯䏇驃犥㵱㬓䴩鰾㺓㹾皫㩠魒勡彯飄顠翲㼼醥徱篻闝僄飃縹旚",
"pie": "撇瞥苤氕丿暼鐅撆嫳覕䥕",
"pin": "品贫频拼聘拚嫔颦姘牝玭榀蘋薲驞礗砏琕顰䀻矉蠙嚬汖㰋馪穦嬪娦貧",
"ping": "平评凭瓶屏苹萍乒坪呯鲆枰娉俜蓱荓聠砯胓䶄塀玶㻂淜涄洴蚲蛢輧軿甹岼幈帲帡屛焩鮃檘缾䍈甁簈箳郱頩艵慿憑凴竮㺸評冯",
"po": "破迫婆坡颇泼朴泊魄粕珀鄱钋笸陂叵钷皤蔢尀蒪頗駊奤砶䞟䨰㨇洦湐溌潑昢哱嘙嚩岥䯙岶䪖烞鉕釙鏺廹敀櫇䣮䣪酦醱醗箥䎊䄸㰴㛘㔇繁",
"pou": "剖掊裒犃垺哣㧵抔捊抙箁咅娝婄",
"pu": "普铺扑谱朴葡仆浦蒲埔菩瀑圃噗曝匍蹼溥濮璞莆氆攴镤镨堡攵䔕䑑蒱䧤陠㹒暴圤墣㺪瞨潽㬥䗱圑贌烳炇㲫䴆菐鯆鏷䲕鋪獛鐠擈撲酺檏樸㯷䈻䈬穙痡暜舖舗㒒僕纀諩譜",
"qi": "起其气期器企七奇汽齐妻启旗弃骑欺漆棋岂凄契歧戚栖泣砌祈蹊乞迄崎祺鳍伎缉岐琦祁琪憩畦沏绮脐亟嘁荠杞麒颀耆啐蛴碛淇葺芪祇綦欹槭萋讫圻蕲揭萁芑骐亓丌柒汔蜞屺桤藄䩓䓅鄿䕤蘄䔇䒻炁芞藒䒗萕陭隑䏅䧘亝騹騎騏䭼䭶唘碶磩碕鬐䰇磧慼䫔䚉栔㓞㼤矵攲敧鵸碁䫏磜剘蜝㐞䳢棊肵䏠臍墄埼霋䞚䟄䎢璂䚍玘郪鶈䀙䶞盀盵濝淒呇滊湇湆蚑螧蚔蚚暣㫓蠐咠唭踦跂䟚噐呮罊蟿䡋軝䡔䢀㟢豈帺㟓岓嵜㠎邔慽㥓愭悽愒迉忯㞚㞓懠粸䉻麡䧵褀褄䙄禥䄎䄢鏚錡锜釮鲯鯕鶀䱈鰭䲬䰴夡玂猉鐑頎掑捿氣鬿魌気摖㩩櫀㯦㟚棲㩽榿檱㮑䣛桼憇諬䅲欫甈㣬䄫簯䅤䑴䉝艩簱籏㾨竒疧闙䀈婍娸傶僛倛䏌䬣㒅綺緀紪䭫䭬綥䌌斊棄䛴諆斉齊䶒䐡䁉䋯啔啟䏿䁈晵啓棨訖旂枝俟稽",
"qia": "恰洽掐卡髂拤袷咭葜鞐圶硈胢䨐殎䶝䶗䠍跒䯊峠㡊帢㤉擖酠冾㓣䜑",
"qian": "前千钱潜迁浅签纤牵欠遣铅歉谦乾倩嵌虔钳黔谴堑扦阡茜钎掮犍钤佥荨骞愆箝芡芊肷椠岍悭慊褰搴仟缱䪈韆䕭茾孯臤蜸掔婜蔳葥蕁蒨騚騝㸫鬜鬝厱膁㦮墘䥅亁乹圲䨿䁮䖍歬淺灊潛汧壍嬱汘濳蚈黚輤塹㟻槧㜞軡㡨岒慳悓忴粁䊴䞿騫錢鹐鵮銭鉗鑓鰬釺鎆鈆鉛鈐鏲㧄攑㩮拑皘㨜攐攓㩃拪揵扲撁橬檶遷棈榩櫏杄槏㯠圱刋谸籖䍉篏䈤䈴篟簽籤羬䇂䦲竏㪠䫡奷媊僉俔儙諐伣㐸偂傔䭤仱欦綪繾縴譴顅謙牽",
"qiang": "强枪墙抢腔羌呛跄锵蔷羟襁戕戗嫱樯蜣炝锖镪薔蘠蔃墻玱瑲溬漒蹡蹌啌嗴唴嗆嶈廧熗獇猐鏘鎗鏹摤㩖搶檣椌䵁槍艢䅚篬牆羥羗羻羫墏斨牄嬙㛨戧強彊繈繦謒疆",
"qiao": "巧桥悄瞧敲乔侨翘峭窍俏锹鞘憔跷撬樵荞橇壳雀诮峤鞒硗愀劁缲谯鞩鞽㤍䲾㚽菬荍藮蕎陗犞磽䃝䩌硚礄䂭翹墝㚁趬趫墽墧睄郻㴥踍蹺躈蹻嘺骹帩幧韒燆㢗㝯䆻竅釥鐰鄥䱁鄡鐈鍬撽櫵槗橋勪喬䀉䎗㡑鍫䇌頝癄嫶僺僑顦繰繑誚髜毃㪣髚譙",
"qie": "切且窃契怯砌伽茄妾惬趄锲箧挈郄苆㥦匧㰼聺㚗洯蛪㓶厒㤲㰰朅淁㫸䟙踥㗫愜悏竊鍥䤿鯜㹤癿篋笡籡穕㾜䦧㾀㛍㛙䬊",
"qin": "亲侵勤秦琴禽钦沁芹寝擒矜噙覃揿芩嗪衾螓吣锓檎菣靲䔷懃斳兓菳菦藽耹骎㮗駸肣㘦赾埐坅琹珡䖌澿瀙螼蠄昑蚙唚㞬嶜嵚嶔嵰懄慬吢㤈㢙庈㝲寴寢寑顉鈙鮼鵭欽鋟鈫抋捦撳㩒搇梫䠴笉䈜瘽䦦親㓎㾛嫀媇㪁鳹雂綅誛",
"qing": "情清亲青轻请倾庆氢晴顷卿蜻擎氰磬罄圊箐苘檠謦黥鲭綮葝䔛碃䌠硘埥殸漀㷫郬靘靑殑濪淸暒甠啨軽輕鑋䝼䞍慶檾庼廎寈錆鯖䲔夝擏掅氫㯳櫦棾樈凊儬傾頃請勍剠䋜䯧",
"qiong": "穷琼穹邛茕跫蛩銎筇卭㧭䓖藭藑蛬䊄䅃赹璚瓗㼇瓊瞏睘惸㷀煢焭熍焪䆳竆窮宆憌桏㮪橩笻䠻舼儝㒌䛪",
"qiu": "求球秋丘酋囚蚯邱裘鳅巯泅湫虬遒楸逑龟蝤赇糗犰鼽俅蓲鞦鞧莍萩蘒芁䎿毬肍䞭趥坵皳䣇盚㺫蟗玌璆殏巰㐀汓浗湭渞蛷虯䟵䟬䠗唒㕤賕㟈崷㞗㤹㥢恘秌煪觩觓銶䲡䤛䱸鰽鯄鮂鰍鰌釚釻㼒㧨搝扏梂逎㭝朹湬蝵鹙鶖醔媝穐篍龝蠤㷕丠頄㐤叴訄恷䜪紌絿緧䊵訅仇",
"qu": "去区取曲趣趋屈驱渠躯娶岖瞿祛蛐觑衢蛆龋黢癯苣蠼佉阒麯蘧蕖磲朐璩氍劬鸲麴诎葋䒧匤菃敺區䒼螶䧢阹驅駆駈厺髷胠刞臞䝣鼩㰦鼁坥䟊䞤趍趨耝璖麹䶚齲覰覻䁦䀠䂂戵鸜覷灈浀淭䖦㫢蠷蟝蝺呿䠐躣㖆軥㻃嶇㲘岴胊鶌憈翑焌爠粬煀袪鑺鴝斪䵶鰸魼鱋抾㭕㯫欔欋麮衐籧忂筁軀闃閴竘竬㜹佢伹紶㣄䋧絇詘詓誳㧁",
"quan": "全权圈泉劝拳券犬醛蜷痊颧铨荃诠筌鬈畎辁悛犭绻勸顴葲虇䄐騡駩犈牷犮硂䑏䟒埢瑔䀬湶洤蠸䠰踡跧啳圏輇巏㟨㟫峑恮䊎烇鳈鐉鰁銓搼權楾権棬椦勧箞㒰齤奍韏觠牶闎婘姾佺縓綣絟詮",
"que": "确却缺雀鹊炔瘸榷阙阕悫皵鵲䧿蒛碏礭確硞碻礐趞㱿㲉愨慤埆㱋塙琷㴶崅燩㕁搉㩁棤㰌缼䇎䦬㾡闕闋傕卻",
"qun": "群裙逡麇䭽夋囷峮宭㿏㪊裠帬羣",
"ran": "然染燃冉髯苒蚺䖄㲯蒅㸐䒣㿵髥肰䫇珃蚦呥嘫冄䎃衻袡袇䤡橪㯗䑙㾆媣姌㚩㜣䣸繎",
"rang": "让壤嚷攘瓤禳穰蘘鬤㚂壌瀼躟懹爙獽穣䉴儴勷譲讓",
"rao": "绕扰饶桡娆荛蕘隢㹛遶襓擾橈䫞嬈㑱饒繞",
"re": "热惹喏若熱",
"ren": "人认任忍仁韧刃妊纫壬饪仞衽荏稔轫亻靭靱荵芢㸾牣䏕䏰肕腍忈䀼軔㠴岃屻韌㣼䴦袵祍魜鈓銋扨梕杒栣朲棯忎躵秹䇮秂姙刄䋕鵀㶵栠飪餁䭃仭䌾紝纴綛紉絍認訒讱",
"reng": "仍扔芿陾辸礽㭁䄧㺱䚮",
"ri": "日䒤驲馹囸衵鈤釰釼",
"rong": "容溶荣融绒熔蓉茸戎榕冗嵘镕蝾肜狨茙㲨䩸氄駥毧㲓㲝坈瑢瀜栄螎曧蠑䠜㘇䡥䡆軵嶸峵烿爃嵤榮㣑䘬褣䇀宂㝐䢇㼸鎔㺎搑搈榵㭜䤊槦穁䇯穃䈶羢媶嬫嫆傇傛縙絨",
"rou": "肉柔揉蹂鞣糅葇鶔騥䰆腬脜䐓瑈瓇渘蝚㖻輮㽥禸韖煣粈宍鍒鰇楺䄾䧷媃厹譳",
"ru": "如入乳儒辱汝蠕茹褥濡嚅孺铷缛襦女蓐薷颥溽洳蕠蒘㹘㦺鄏肗䰰顬渪蝡曘嗕嶿袽鱬銣㨎擩扖醹杁筎㐈鳰邚鴑䋈媷嬬帤鴽挐桇侞縟繻",
"ruan": "软阮朊䓴碝礝耎腝堧壖瑌瓀輭㽭軟䞂撋䪭㼱媆偄㐾緛",
"rui": "瑞锐蕊兑睿芮蕤蚋枘蕋蘃蘂䓲㓹甤叡㪫㲊壡汭蜹繠橤鋭銳桵㮃䅑㛱緌䌼",
"run": "润闰膶瞤潤㠈橍閏閠䦞",
"ruo": "若弱偌箬鄀爇蒻叒䐞渃㘃嵶焫鰙鰯挼捼楉篛婼鶸",
"sa": "萨撒洒仨卅飒脎蕯薩靸躠隡馺䘮㪪灑㳐䊛䙣鈒钑摋櫒颯㽂㒎䬃訯",
"sai": "塞赛腮鳃噻毸毢嗮㗷嘥顋愢賽䚡鰓揌䈢簺僿思",
"san": "三参散伞叁糁毵馓弎㪚毿犙䫩鬖毶壭䀐潵㤾糤糣糝䊉䫅鏾鏒㧲㪔䉈閐厁俕饊傘繖",
"sang": "丧桑嗓搡颡磉顙䫙桒喪䡦褬鎟槡",
"sao": "扫嫂骚缫搔臊瘙埽鳋䕅騒騷䐹矂溞螦氉鰠鱢掻掃㿋㛐㛮颾繅髞梢",
"se": "色涩瑟啬塞铯穑槭䔼雭䨛嗇㱇璱㻭歮濇濏澁渋㴔洓瀒澀轖懎㥶銫鏼摵擌㮦栜穯穡䉢閪瘷歰飋㒊繬譅",
"sen": "森襂槮椮",
"seng": "僧鬙",
"sha": "沙杀啥纱砂傻刹厦杉莎煞鲨霎裟挲嗄唦痧唼铩歃萐蔱䮜髿䝊硰㲚㸺乷鯊桬啑喢帹翜翣廈粆魦鯋鎩猀毮閷殺榝樧㰱箑䶎䈉䵘閯㚫㛼儍倽紗繺",
"shai": "晒筛曬㬠㩄簛籭簁篩",
"shan": "山单善闪扇衫陕珊禅杉擅掺栅煽膳删姗汕赡跚掸讪缮舢疝嬗潸鳝搧鄯苫膻芟骟彡蟮钐陝剼騸㚒磰㪎脠赸墠圸墡㣌睒灗澘㶒晱蟺嘇軕刪邖幓贍炶煔覢熌䘰禪䄠釤銏䱉䱇鱓鯅鱔狦鐥䦅䥇䦂掞挻㨛樿柵檆椫䴮㣣笘䠾䆄痁閊㪨敾歚羴閃羶譱姍僐饍傓縿繕䚲訕謆",
"shang": "上商伤尚赏汤裳晌熵墒垧殇觞绱鞝蔏鬺殤丄尙賞漡滳螪贘慯恦禓觴鋿鏛鑜樉䬕傷緔扄謪",
"shao": "少烧绍召稍梢哨勺捎邵鞘芍韶筲艄苕劭潲杓莦萷䒚䔠蕱髾㪢䏴㲈玿輎㷹焼燒䘯袑鮹柖䈰䈾㸛娋卲綤䙼䬰弰紹旓",
"she": "社设射涉舍摄舌蛇折拾畲奢赦慑麝赊佘猞歙阇厍滠揲蔎騇厙奓䂠䁋䁯灄渉㴇㵃涻蠂虵蛥䵥畭輋䞌賒賖懾韘慴䀅䄕䤮攝摂捨欇㰒㭙檨䠶㒤舎畬䬷弽䌰㢵設",
"shen": "什身神深参甚审伸申沈渗婶肾慎绅呻娠砷蜃莘吲糁鯵诜谌瘆信葚胂渖哂矧谂蔘腎頣蓡薓葠駪㰮眘昚脤㥲堔珅眒瞫滲㵕㵊瀋涁蜄曑曋罧屾峷愼糂籸燊籶邥㔤審覾宷裑䆦穼罙祳鋠鲹鉮鰺鰰魫鯓氠扟䰠柛㰂榊兟甧甡鵢瘮㾕妽嬸㜤姺敒侺侁㑗紳弞矤訷谉讅詵諗訠",
"sheng": "生声省胜升盛圣剩乘牲绳笙甥嵊晟眚蕂苼䎴聖陞阩陹鼪勝賸榺墭聲殅珄渻湦泩䚇㼳晠琞曻昇㗂呏貹䞉憴焺鍟䱆鵿鉎狌斘橳枡剰䪿㾪竔偗䁞繉縄繩譝甸",
"shi": "是时实事十使什式世识食市史石始师失视示似适士势试施室释诗氏湿饰驶拾蚀尸逝侍誓矢狮匙柿硕嗜屎噬嘘栅拭峙仕恃虱轼舐耆螫豕谥弑奭殖蓍泽莳贳埘炻鲥鲺铈酾筮蒔貰䒨蒒葹䦹旹㱁乨駛䰄觢㸷䩃乭䂖䏡鼫鼭卋㔺邿塒㐊辻兘勢丗䴓鳾瑡亊䶡眎睗䁺眂眡溼溡浉湜濕㵓澨溮湤䖨㫑㫭時昰遈㒾呩㕜䟗㖷呞軾嵵崼峕忕蝨屍鸤䲩鳲恀烒煶䊓実寔宩冟襫襹褷䙾實祏視鉽釶鉐鉂䤱鮖鰣鯴鰘鰤鶳鉇鉃㹬㹝獅鈰鍦㹷銴弒揓栻枾釃榯榁柹㮶簭遾舓秲徥師釋釈笶籂箷竍䦠嬕姼餝䭄蝕餙飾飠䌳絁試詩諟戺䗐䛈適謚諡識",
"shou": "手受收首守授售寿瘦兽狩绶扌艏膄壽夀垨涭獣㖟獸㥅収㝊鏉龵痩䭭綬䛵",
"shu": "术数书属树述熟输束殊叔朱舒鼠疏署竖蔬抒枢淑暑薯梳俞蜀庶赎塾墅恕曙倏漱黍腧戍孰澍秫菽纾疋沭摅姝殳毹荗䩳䩱㷂竪豎䜿䝂薥䔫蒁藷陎㽰毺䑕䞖霔尌朮怷璹琡䜹㻿尗虪濖瀭潄潻㳆鼡㶖蠴暏䠱踈䟽咰數軗輸㟬贖䝪䎉疎屬庻糬襩裋襡䘤䆝鏣鮛鱪鱰錰鉥掓攄捒樞樹橾㯮杸䴰鶐䉀䢤術癙㾁書㛸婌㜐㣽鵨鄃侸跾倐儵焂㒔紓綀絉䃞",
"shua": "刷耍唰㕞誜",
"shuai": "率衰摔帅甩蟀帥䢦卛",
"shuan": "拴栓涮闩䧠腨閂",
"shuang": "双霜爽孀骦騻驦礵䫪鷞㼽㦼塽鹴鸘漺灀䗮䡯慡鏯欆艭㕠孇雙縔",
"shui": "谁水睡税说氵脽氺涚涗帨裞祱稅閖㽷䭨誰",
"shun": "顺瞬舜吮蕣䑞鬊䀵瞚䀢順㥧橓楯",
"shuo": "说烁硕朔数妁蒴铄搠槊矟碩䀥爍鑠獡鎙欶箾䌃説說",
"si": "思四死斯似司丝私饲寺撕祀肆嘶嗣厮俟泗咝巳鸶蛳驷锶汜伺食厶耜兕澌笥姒缌纟蕼䔮蕬㹑㸻牭騃駟騦磃蟴䏤鼶貄亖耛䎣㺨肂洍涘洠瀃泀泤㴲蟖螄㕽噝罳㟃孠覗廝燍䇁禗禩禠鈶鐁鋖鍶鈻鉰釲㺇銯虒枱杫梩柶楒㭒榹蜤㐌恖竢凘䦙䇃媤㚶㚸娰儩佀飔価俬颸飼飤緦糹㣈鷥絲",
"song": "送松宋颂讼耸诵淞嵩悚凇怂忪菘崧竦駷鬆硹濍㕬嵷憽㞞愯䢠庺梥鎹㧐㩳㨦檧楤㮸枩柗㣝䉥聳慫娀頌枀倯傱餸䜬誦訟䛦",
"sou": "搜艘嗽嗖擞飕馊薮螋叟溲瞍嗾锼蓃藪蒐䏂騪䮟鄋㵻㖩廋廀叜鎪獀捜擻摉摗醙櫢籔䉤䈹凁瘶傁颼䬒餿",
"su": "素速苏诉缩俗塑肃宿稣溯粟酥簌窣夙谡嗉僳愫蔌涑觫蘇莤藗䔎蘓㕖骕驌碿䃤膆塐趚甦殐璛珟玊溸㴑㴼泝潥潚㴋洬㬘囌蹜憟䘻㝛鯂穌鱐鋉䥔㨞㩋榡遬㔄樎櫯梀㯈樕䅇橚䑿愬遡㪩肅䎘鷫嫊鹔䏋粛㜚㓘傃㑛餗㑉縤䌚䛾謖訴",
"suan": "算酸蒜狻匴㔯祘筭笇痠",
"sui": "随虽岁碎遂髓穗隋绥隧邃祟燧睢荽濉谇䔹荾鞖䪎芕隨䢫鐆遀砕膸埣瓍㻟璲㻪㻽歲歳睟瀡㵦浽澻㴚滖哸䠔雖䡵嵗㞸䯝髄亗賥韢熣煫襚禭䥙鐩夊檖䉌穂䅗穟㒸嬘䭉倠綏繐繀繸䍁䜔旞譢誶尿",
"sun": "孙损笋荪狲飧榫隼蓀薞蕵孫飱䁚㡄㦏猻鎨搎損㔼槂簨箰筍鶽",
"suo": "所索缩锁梭嗦琐唆羧唢娑蓑挲些睃睃嗍桫䓾莏䂹䐝䞽趖琑瑣㪽䖛溹溑逤䣔暛蜶嗩䞆惢褨鎍鎖鮻獕鏁鎻挱乺摍䵀䅴䈗簔簑㛖傞䌇縮莎",
"ta": "他它她塔踏塌榻沓蹋嗒拓獭挞趿遢溻鳎铊闼鞳鞜䓠㿹牠䂿䶁䶀墖㳠㳫澾涾毾躂躢蹹嚃嚺㗳䵬䍝遝崉䎓粏褟祂禢錔鰨鮙鉈㺚㹺獺狧撻㧺搨榙橽㭼㯓䑜䍇㣵濌䈋䈳㣛闧闥闒阘㛥侤㒓傝䌈誻䜚譶",
"tai": "大太态台抬泰胎苔汰钛酞肽薹骀邰炱跆鲐䑓菭孡態㣍駘夳冭坮臺㙵溙汏汱㬃旲㘆囼㥭忲䢰燤炲㷘㸀鈦鮐擡檯䣭䈚籉箈舦嬯㒗㑷㑀儓颱",
"tan": "谈探弹碳坦叹滩炭摊坛贪谭潭痰毯瘫檀坍袒覃忐昙钽澹郯锬藫歎菼䕊䃪貚䏙䐺墵䞡壜埮墰㽑壇璮灘潬湠曇暺嘆嘽啴嗿惔憛憳憻顃㲜㲭㷋燂䊤襢䆱鉭錟擹攤醈醓醰橝榃舑舕罎罈䉡癱䦔痑婒怹倓僋貪談譚䜖譠",
"tang": "堂唐糖躺汤塘倘趟烫膛淌棠搪螳蹚羰傥溏帑醣耥瑭螗铴镗樘鞺薚蓎隚䧜磄膅鼞赯矘漟燙湯坣䣘劏曭蝪踼䟖嘡啺戃糛爣糃煻鄌㲥鶶㼺禟鐋鏜钂鎲镋鎕㿩摥㭻橖榶䉎篖䅯闛㜍伖㑽儻㒉偒傏饄餹䌅㙶",
"tao": "讨套逃陶桃萄掏涛淘滔叨韬啕绦洮饕跳鼗鞱鞉鞀㹗騊駣㚐夲瑫㴞濤蜪飸咷轁幍慆韜裪祹迯鋾匋搯槄醄䵚嫍絛䬞饀䬢弢縚綯绹縧詜謟討䚯䛬",
"te": "特忑忒慝铽脦蟘㥂鋱㧹",
"teng": "腾疼藤滕誊䕨虅驣䲢幐縢螣騰鰧謄膯鼟霯漛䠮熥籐籘䒅䲍駦痋邆儯",
"ti": "体提题替梯踢蹄惕啼剔剃涕屉嚏锑棣倜悌鹈逖醍缇绨䪆䔶薙蕛䧅㯩騠髰鬄鬀碮厗朑䨑趧趯䎮瑅殢瓋睼漽渧題鶗惖逷㗣嚔蹏鷤嗁㖒罤䯜體骵䝰㡗崹惿屜褆䙗褅禔禵䚣鳀鯷鷈鮷悐銻鍗䴘鷉掦挮揥擿笹䣽㬱䶑䅠躰軆徲籊稊㣢䣡䶏鵜媞偍䬾緹䌡綈䛱戻謕歒鶙弟",
"tian": "天田甜填添佃恬腆舔阗钿畑忝殄畋掭菾黇磌碵䩄胋鷏㙉甛塡靔靝瑱㐁琠璳兲睓沺淟湉晪䟧䠄唺㖭䡒䡘鴫䐌覥觍賟悿屇㥏㶺窴錪䥖搷㮇䣯酟䑚舚䄼䄽闐痶婖倎餂鷆緂㧂甸",
"tiao": "条调跳挑眺迢窕苕佻笤啁粜髫龆蜩祧鲦䒒萔芀蓚蓨糶聎䯾朓趒齠晀旫䟭㟘脁岧岹恌庣宨窱祒覜鰷䱔樤㸠䠷䎄䳂嬥鞗䩦䖺鯈鋚鎥條絩誂",
"tie": "铁贴帖餮萜聑驖䵿蛈呫貼怗鐵鐡䥫銕鉄䴴僣飻",
"ting": "听停庭挺厅廷亭艇烃婷蜓汀霆町铤葶莛梃鞓聴聽聤厛鼮脡䵺圢耓珽涏渟䗴蝏甼嵉聼廰廳烴庁烶䱓鋌㹶邒桯榳楟頲颋筳䦐閮娗侹䋼綎誔諪",
"tong": "同通统痛童铜筒桶桐佟侗酮捅瞳僮彤潼嗵恸峒茼砼仝蓪㼧㪌䂈䮵犝朣赨眮浵晍蚒䳋曈哃㠽峝峂㠉膧慟㤏烔粡庝炵燑䆚䆹鲖鉵銅鮦狪獞鉖樋㮔橦筩憅㣠秱㣚衕穜䶱勭氃䴀㼿痌㛚㸗餇絧統綂詷",
"tou": "头投透偷愉骰亠蘣斢黈䞬頭㰯䟝㖣㡏䵉㢏鋀䱏鍮㪗敨婾媮妵㓱偸紏緰㕻䚵",
"tu": "图土突途徒吐涂兔屠凸秃荼钍菟堍酴蒤葖莵鷋駼鼵迌腯㐋堗圡瑹㻬㻯㻠㻌䖘汢潳涋湥塗跿䠈唋圗圖図嶀㟮䣝鷵怢悇廜庩宊鶟鈯釷鵵鵌鍎鋵揬捸捈䤅㭸梌䅷馟兎禿稌筡鵚瘏痜凃䣄峹嵞䳜",
"tuan": "团湍疃抟彖䵎貒墥剸鷒漙湪団䵯團畽圕慱䊜糰煓褖鏄鷻猯摶㩛槫檲篿䜝揣",
"tui": "推退腿颓蜕褪忒煺藬蓷蘈隤駾㞂尵㦌䍾䀃㱣螁蛻蹪蹆骽㷟㢈㢑魋橔頺䅪頹䫋頽穨㿉㾯㾽㿗㾼弚娧俀僓弟",
"tun": "吞屯豚臀囤褪饨鲀氽暾芚朜霕坉㼊豘涒旽蛌㖔噋黗軘臋忳㞘焞魨㹠㩔呑飩",
"tuo": "脱托拖妥拓驼陀唾椭驮沱砣鸵佗坨跎箨柁柝橐沲鼍庹酡乇䓕萚蘀莌阤嶞陁馱駄䭾驝騨驒駝馲駞㸰㸱毻碢砤鼧鵎脫堶槖沰汑涶跅鼉咜咃䡐㟎岮䪑袥袉㼠饦䲊鰖鮀鴕魠䰿鮵狏扡拕捝挩橢楕杔䴱彵籜䍫㾃嫷媠毤侂仛侻飥紽詑託讬",
"wa": "瓦挖娃哇蛙凹洼袜佤娲腽韈聉䎳砙膃劸㰪鼃䵷邷漥溛咓䠚嗢嗗畖㼘韤襪窐窪穵窊㧚搲攨屲瓾媧䚴",
"wai": "外歪崴呙㖞喎咼䶐䠿顡竵",
"wan": "完万晚玩湾弯碗顽挽烷婉皖蔓腕丸宛惋蜿豌绾纨莞剜脘畹塆菀芄琬箢薍萖萬㿸䂺䩊脕埦頑㝴刓壪琓瞣睕澫涴潫汍灣蟃晥晼晩踠唍輐輓贎䯈岏貦帵贃䝹忨卐卍翫䗕䘼䖤盌㽜鋄錽䳃鋔䥑鎫抏捖捥杤椀梚䅋笂妧婠倇㸘綰綩紈䛷彎",
"wang": "往王望忘亡网旺汪妄枉惘罔辋辋魍朢菵莣尪迋尫瀇㲿㴏㳹蚟蛧蝄暀罒輞罖罓㓁䤑棢徍彺䰣徃兦仼亾尣尩䋞䋄網䛃誷",
"wei": "为维围委未微谓卫味唯威危伟尾违伪慰魏喂胃纬畏韦惟苇萎尉蔚巍薇偎帷娓渭桅圩倭痿崴猬诿猥潍煨葳韪帏嵬玮逶炜隈隗洧涠沩囗軎鲔艉闱位䔺蔿䪋苿菋蓶葨䵋荱藯葦蘶芛蒍隇䧦䮹熭碨硙䃬㞇硊㕒䑊腲鄬爲䙿壝墛霨䞔霺䝐瑋㱬琟覹矀濻潙韑瀢渨潿溦浘湋洈濰溈蝛㬙韙蝟暐蜲蜼喴踓㖐喡䡺轊囲䵳罻圍㠕骩骫骪幃嶉嵔㟪峗峞嶶崣屗㞑叞褽犚螱㷉韡䪘韋違㥜愄愇懀燰烓煟煒寪頠鏏厃鳂鳚鍡鍏鮪鰄鮇鰃䲁鮠䥩撱㨊揻揋捤㧑楲㭏醀椳欈梶椲䈧㣲徫躗躛㦣衛衞䘙讆讏䉠覣犩䭳痏闈癓媙媦媁儰僞偉䬑颹䬐饖餵䬿餧偽縅緭緯㢻維䗽詴亹斖䜜謂䜅為諉",
"wen": "文问温闻稳纹吻蚊紊瘟韫雯汶刎璺阌鞰莬芠䎽駇馼鼤脗肳塭豱瑥䰚殟珳渂溫㳷昷㗃呡㖧呅輼辒轀蟁炆顐㝧鳁鎾鰛鰮魰揾搵抆榅榲桽穩穏䎹聞閿闅䦩閺問闦瘒妏㒚伆饂繧紋彣䘇螡蚉㐎鴍鳼",
"weng": "翁嗡瓮蓊蕹聬㹙㹚䐥䤰塕奣瞈滃暡螉㘢嵡䱵鎓攚齆䈵㜲勜鹟鶲罋甕",
"wo": "我握窝卧沃涡斡蜗喔倭挝龌渥莴幄硪肟臥萵䰀臒腛㦱瓁㱧齷䁊瞃濣渦涹蝸䠎踒唩㠛焥窩猧捰捾㧴枂楃婐媉婑仴偓",
"wu": "物无五务舞武屋误恶午吴伍污乌雾悟吾呜侮唔巫勿梧诬捂晤兀於芜戊毋鹉妩钨邬坞蜈婺鹜忤骛牾庑杌亡芴阢鼯圬浯鋈怃焐寤迕痦仵莁靰蘁茣蕪鹀鵐陚䎸隖奦務㡔嵍熃騖鶩䳱敄䮏鴮碔矹䃖䑁㬳霧雺霚塢墲鵡珷珸郚㻍㐚逜㐏忢瑦卼玝璑瞴洿汚汙洖溩㵲潕螐旿蟱䟼躌吳呉嗚䡧䍢峿屼嵨岉剭悮悞憮乄熓粅廡㷻窏窹祦鋙铻鄔鯃烏鰞歍鎢㹳扤摀㐅杇啎無鷡橆甒鼿齀箼䒉㽾䦜䫓䦍娬娪嫵娒倵俉㐳儛㑄弙䳇誣誈䛩誤譕",
"xi": "系西细习息吸喜戏析希席洗稀惜悉袭腊溪媳牺锡嘻夕隙晰栖膝熙昔烯熄禧鳃徙嬉犀蟋奚兮曦蜥汐翕玺唏螅铣淅硒皙熹窸羲矽檄郗忾僖屣歙樨觋娭豨咭葸菥蓰隰鼷舄浠粞裼穸禊饩欷醯舾阋㐂葈蕮蒵䩤䓇匸煕蓆莃薂蒠覡隵隟䧍䢄枲騱驨騽䮎犔犠犧磶磎礂䲪䙽㚛䐼䏮貕舃肸肹谿䫣㙾霼趘䨳趇欯囍憙歖霫赩赥豯卌琋壐璽瞦䀘鬩戲䖒矖戱卥戯睎盻覤㳧澙渓潟鸂虩漝㵿漇潝螇暿蟢蠵晞嚱躧蹝呬㗩㕧焁唽噏喺繫黖㽯嵠巇㠄嶍酅㔒忚㤴慀恄憘㤸怬屃屓屭㥡㦻悕習飁恓㞒屖焟熺糦㸍焬熂燨爔熻邜鐊觿觽觹鳛錫鑴饻鱚鰼鯑鉨釸鈢㹫㺣鎴釳鏭狶鉩扱鵗㩗忥氥扸墍㯕榽䙵橲槢桸晳惁椞㮩㭡厀椺橀怸熈㷩稧徯㣟䈪郋鄎徆襲㿇凞瘜闟䊠㜎衋嬆傒翖俙㑶係饎餼餏郤豀縘繥緆細縰綌绤謑䜁譆諰焈謵䛥䜣䚷洒蹊",
"xia": "下夏吓狭辖霞峡瞎厦虾暇匣唬遐侠黠呷瑕罅狎瘕硖柙蕸陿陜䖎騢硤碬磍夓埉圷㙤赮丅乤珨睱䖖虲蝦㗇㽠㘡翈轄峽懗䫗㰺䪗舝炠煆烚鶷䘥祫鎼鏬鍜魻鰕鎋狹梺筪敮舺閕䦖疜閜傄俠颬谺縖諕䛅",
"xian": "现先线显限县鲜险献宪陷仙闲纤腺弦贤嫌掀咸衔羡掺涎娴见酰舷藓馅锨铣冼霰暹籼苋痫氙蚬岘莶燹跹跣祆猃筅鹇藖韅䁂賢贒莧䵌㔵蘚䒸䕔薟苮䧟䧋䧮陥険險礥䃱尠䃸臔姭䏹鼸毨胘韯壏塪赻䨘垷埳䨷現豏珗䶢䶟獻睍縣鹹県盷瞯涀灦㳭瀗㶍㳄鍌㵪澖湺䝨尟㫫晛蜆䗾顕䘆㬗蛝顯㬎蚿㘋咁咞嘕哯蹮躚啣㘅嗛輱䞁幰峴㡉崄嶮㦓忺憪憸糮粯廯䵇烍㡾麙鶱憲褼襳禒鑦臽䚚䀏鋧䥪䱤鱻䲗鮮銽錎䤼鍁銛铦銑獮玁狝㺌獫㧋搟攇㩈㧥撊撏挦攕㮭醎枮櫶杴㭠橌橺麲㭹㯀䉯䢾㪇箲馦秈銜䉳衘稴屳閒鷳羨鷼閑鷴㜪䦥㿅癇癎甉㛾娊奾嫺嫻嬐孅娹妶仚僊僲僩僴餡韱佡伭綫纎繊線缐絤㢺纖婱絃諴誢䜢譣誸洗",
"xiang": "想相象向响像项乡降香羊享箱祥详湘橡翔巷厢镶襄饷骧芗飨衖葙蟓庠鲞缃缃項瓨䔗萫䢽薌驤䐟膷䜶珦瓖晑䖮曏跭㟟嶑㟄䊑廂麘襐勨鱌鱶鱜鐌銄鑲栙楿欀缿稥忀鮝鯗姠佭餉饟緗鄊蚃鄕郷鄉蠁響嚮㗽饗絴纕亯㖜㐔詳",
"xiao": "小消笑效校销削晓肖硝萧孝啸潇俏嚣哮淆宵箫霄筱逍骁姣枭哓鴞蛸崤魈枵绡绡䒕虈䕧䒝蕭藃驍硣膮斅斆㬵毊瀟揱涍㕾敩洨蠨蟏暁曉蟂蟰嘵嘋鸮踃嚻囂呺嘐㗛咲嘯嘨髐髇憢㤊恔庨焇灲熽䊥灱宯窙銷鴵䥵梟㹲猇獢郩殽皢皛撨櫹穘鷍筿簫簘篠痚痟効㔅歗婋虓侾翛㑾烋颵俲傚綃彇謏誟歊誵訤詨",
"xie": "些解写协谢械鞋斜谐胁泄歇邪契携卸屑泻蟹懈挟蝎偕楔勰亵燮鲑撷颉榭邂缬澥瀣廨躞叶薤渫獬榍绁靾鞢鞵䕵䩧䢡藛薢䕈䔑㔎㕐絜脅脇劦膎協㙝奊翓塮暬垥瑎齛齥齘禼卨䪥韰㱔㳦洩㴮瀉㵼㴬㴽㳿蝢旪蠍蠏㖑嚡噧㖿嗋䵦䡡峫嶰屟恊愶屧㞕㥟㦪灺緳熁燲糏炨炧䊝冩寫㝍褉䙎襭䙊祄㙰䲒䥱䥾猲揳挾拹㨙擷攜㨝烲焎娎㩉㩦擕㩪㰔䉣缷徢齂㣯䉏㣰䦑㸉㓔䦏媟孈脋伳偞偰龤㙦㒠㰡僁䭎紲緤綊纈絏縀繲絬衺䚳䙝褻讗爕夑㽊謝䚸諧血",
"xin": "心新信欣辛薪锌芯馨鑫衅昕訢忻莘炘歆囟忄镡䒖阠孞馸舋釁脪盺噷噺軐惞廞焮襑鈊䰼鐔鋅邤㭄杺枔馫顖嬜妡㛛㚯㐰伈俽伩䜗䚱訫䛨",
"xing": "行性形兴型星省幸醒刑姓杏猩腥邢惺悻荥陉擤荇硎饧䓷莕葕陘骍騂臖興㐩㓝㼬㙚垶㼛郉瑆䣆䁄睲涬洐蛵曐哘䳙煋滎㝭觲觪䤯鈃钘鉶铏銒鋞鯹鮏㨘䰢皨㮐䂔㣜箵篂㓑嬹婞娙倖侀餳緈䛭謃",
"xiong": "雄兄胸凶熊汹匈芎熋䧺洶焽焸哅賯恟忷夐敻胷匂兇詗诇詾訩讻㐫",
"xiu": "修秀休袖臭羞绣朽锈嗅溴貅岫咻宿髹庥馐鸺苬髤脙璓臹珛㱙琇潃滫螑嚊㗜峀糔烌鱃鮴鏥銹鏽鎀鏅銝樇齅㾋脩鵂俢飍饈綉繡繍褎褏",
"xu": "许需须续序虚徐绪叙蓄吁絮婿嘘旭栩墟畜浒戌胥圩恤煦蓿酗顼诩魆洫盱砉溆勖糈醑芧蕦藇藚㰲蒣聓䔓㜿䦽㞊䳳㷦㕛㐨䂆驉㚜㦽鬚䢕盨媭嬃須㘧壻垿珬頊珝殈㺷瞁虛歔虗汿沀㵰湑潊漵朂晇暊勗旴冔蝑昫㖅噓㗵呴喣盢㞰賉怴㤢㥠慉燸烼歘欻烅裇䙒禑銊鑐欨鱮䱬獝揟魖䣱䣴楈槒聟䅡鄦卹䘏欰稰稸疞㾥䦗䍱姁㜅㑔㑯敍敘伵偦䬔侐俆䋶續続緒緖縃綇䜡訏譃諿詡諝谞訹許䛙休邪",
"xuan": "选宣旋悬玄喧轩绚眩炫渲漩暄萱癣煊镟璇县碹泫铉揎楦痃儇谖萲䩰鞙䩙蓒蕿藼蘐蔙䧎駽䮄塇璿琄瑄琁玹懸睻眴矎贙䁢㳙㳬晅昍蠉暅蝖蜁暶昡咺䠣吅軒翾䴉㘣䍗䝮愋懁選愃怰烜翧䘩袨禤䚭䚙鋗䴋鰚䲂鍹㹡鏇鉉㧦楥梋檈箮衒䍻癬㾌媗嫙颴弲繏絢縼諼譞諠䗠䲻券",
"xue": "学血雪削穴薛靴谑踅噱鳕泶蒆鞾茓辥膤學觷壆澩嶨燢鷽䨮趐坹瞲㔧辪㶅瀥峃鸴㗾㖸吷轌㞽㡜岤䎀袕鱈䱑狘㧒㿱乴樰䤕桖艝疶䫻䬂䫼䭥斈謔",
"xun": "训迅寻循讯巡询旬逊驯勋熏汛殉荀薰峋洵浚鲟徇浔醺窨荨埙巽蕈孙曛恂郇獯蘍薫愻遜馴駨顨奞毥臐壦攳坃塤壎殾燅珣璕矄潠潯畃䖲蟳勛噀嚑噚䞊卂巺㽦爋燻燖䙉㝁迿㰬鱏鱘鑂狥㨚灥揗㰊杊栒樳桪稄勲勳鄩尋廵焄㜄侚伨偱㒐䭀紃䋸纁㢲訓訊詢䛜訙",
"ya": "压呀亚牙雅芽鸭押崖哑鸦讶丫涯轧衙娅伢蚜桠氩垭碣琊疋迓邪砑睚吖岈揠痖蕥䪵鴉聐孲厊圧厓䃁壓厑䝟劜堐埡圠玡亞鵶䢝㰳亜襾齖齾漄啞唖圔䵝軋鴨崕䯉㿿庌䊦庘㝞窫錏鐚铔䰲犽猰猚㧎掗氬挜枒椏覀笌䄰稏䅉冴疨瘂䦪婭俹訝",
"yan": "眼研验言严演烟沿盐延颜岩炎燕掩厌艳咽焰铅宴衍殷阎雁淹砚檐焉彦蜒俨奄谚腌堰晏胭嫣阉湮筵兖妍偃唁鼹恹琰赝魇滟酽焱餍甗郾菸厣埏鄢罨崦剡闫谳讠鹽匽鶠䕾酀㬫鷰㷼䴏嬊莚萒蔅䓂隁隒驠騴騐験驗牪硽黡䊙揅硏硯夵魘厭厴懕黶檿嬮饜䣍剦礹䂩鳫贗鴈贋㷳䶮䂴臙䑍鼴墕壧䎦䀋塩壛㿼䢥珚琂齞齴䖗鬳䁙覎䀽虤沇厳漹灔灎灧灩淊溎渷㶄㳂渰蝘曣㦔猒䗡暥曮鷃曕妟䳛昖㫟嚥嚈囐嚴碞喦嵒㘙啱㗴喭㘖噞黭黫黬黤艶艷豓豔巘巚巌嵓巖巗觃嵃嶖愝懨熖㷔焑敥炏焔煙烻㢂爓㢛麣戭褗裺鴳䄋䤷觾燄鰋䲓䱲狿抁揜椻㭺歅醼醃釅醶欕棪樮椼櫩楌篶郔䗺躽軅簷䅧䇾閆閹龑䢭兗乵閻顔遃㿕嬿㛪姸孍㚧姲娫娮傿弇顩㕣儼偐䭘酓㓧䳺䨄縯䊻綖䌪讌䜩顏彥訮詽讞扊諺㫃訁",
"yang": "样养阳洋氧央杨扬羊仰秧痒漾疡佯殃鸯怏鞅恙徉炀暘泱蛘烊陽阦駚礢胦䑆霷雵坱垟珜䁑眏眻瀁䬗昜敭蝆䖹旸㬕咉䵮輰軮㿮崸䒋鴦崵岟㟅懩慃煬炴鍈卬鍚鉠钖鰑㺊氜揚氱抰㨾攁楧鸉楊柍様樣䇦劷羏㔦羕飬養瘍鴹癢姎佒飏颺䬬䬺䭐傟紻諹詇详",
"yao": "要药摇腰咬耀遥邀瑶姚窑妖谣钥尧么乐吆肴夭侥舀幺徭珧杳窕窈鹞繇曜爻约轺崾鳐䔄蘨靿薬藥葽蓔苭葯騕磘㞁䂚䍃颻鷂飖尭垚顤堯瑤殀䋤䶧齩䁘㔽矅覞䁏眑㴭溔滧㵸㿢暚䖴㫐嗂喓鷕軺峣嶢嶤岆㟱愮熎燿烑㢓䴠宎㝔䙅袎窰䆙窅䆞穾窯窔祅鎐鰩鱙猺遙獟狕䚻䢣䌛邎揺抭搖㨱摿榣柼㮁楆枖榚鴁鼼䉰筄䑬艞㿑闄媱婹傜倄偠仸䬙餆餚鴢䌊䋂纅謡謠訞㫏䚺讑詏疟",
"ye": "也业夜叶液爷野喝页冶耶咽邪拽曳腋椰掖噎晔谒揶射邺靥吔烨铘䓉葉枼䧨驜靨擪㪑頁礏墷枽㙪㐖璍瑘殗瞸瞱潱澲漜洂曄曅蠮暍曵曗嘢㗼㖶㖡㙒嶪嶫燁煠㥷爗鄴鸈業㱉㝣鐷鋣釾䥺鍱鎁䤶鎑馌䲜䥟䥡䤳擛皣捓抴擫歋㩎捙擨㭨壄埜䈎㸣僷倻爺䭟餣饁謁亪亱鵺",
"yi": "一以义意已艺易议咦依益衣异医移遗疑亦宜仪忆伊倚乙亿抑役毅译椅翼姨蚁泄谊疫逸矣溢夷疙绎尾蛾怡胰贻裔彝邑奕翌屹臆颐诣驿熠咿蜴漪沂呓揖弋轶迤懿悒佚羿噫铱弈壹肄翳癔缢刈旖苡怿痍猗诒峄食射荑薏埸圯殪眙嗌黟嶷嶷衤饴钇镱镒挹酏劓舣瘗翊仡佾蘙芅匜䩟藝蓺虉弌頤巸媐䖁䓃㔴䔬苢勚勩萓苅殹㙠醫鹥瞖繄䗟贀悘鷖黳嫛毉瑿萟䓈藙䓹䕍䬥隿耴迆阣䧧㹓瓵䮊驛駅䭿逘礒䝝帠肊䐖䐅鶂膉貖䝘敼㰻霬墿夁亄㦤鷧㱅壱坄㙯埶㺿玴珆豷豛䰙鹝鷊辷㱲殔鴺乁頥齮齸頉㵩浳㶠渏沶㴁洟浥潩㳑瀷㲼泆浂澺洢㵝㴒湙曀蛦晹䗑曎螘蛡敡鶍螔蟻䗷螠蛜暆囈呭㘊跇遺跠㖂唈㘁呹吚㕥㘈異欭輢黓睪斁歝圛軼轙畩貤貽䞅骮䯆顗峓幆嶧䝯嶬崺怈㦉恞㠯䎈郼䢃懌乛㞔㰝㥴忔攺憶㡼廙熼燡㢞熤燚熪燱炈庡焲宧冝宐㝖襼袣䘝衪裿褹袘寱䘸䆿迱寲䄁祎禕釴鈘釔鉯䱌鶃鮧鯣䱒鳦鸃䲑鮨鏔匇迻狋㹭獈鐿鎰鈠銥撎䖊㣻拸乂㩘枻杙杝槸䣧醳醷桋栧椬栘柂檥檍榏枍䴬㰘椸檹棭䄬劮鄓㓷䇵䄿䇩穓顊稦笖簃乊䉨艗艤秇垼篒籎瘞瘱豙䴊義羛羠鷾痬䦴竩兿鹢鷁嫕㚤㜒嬄㚦㛕彛彞嬟嬑㜋㛄佁侇㑥俋伿㐹㑜䬁儀億飴饐䭂䭞䬮䭇伇㥋偯㑊弬㣂㡫䋵繹䋚䌻彜觺㽈繶縊讛詍裛詒旑訲讉譯㦾扅悥扆訳帟誼誃謻㫊議譩詣蛇",
"yin": "因音引印银阴隐饮姻吟殷荫淫尹茵寅蚓瘾龈垠胤喑氤窨鄞吲圻狺铟茚霪堙洇廴夤夤蘟蔭䕃鞇靷荶蔩䓄蒑䒡隱檃櫽隠阥陻隂陰骃駰㹜碒磤㕂㥯㸒䨸霒趛赺韾堷霠烎璌殥慭珢齗齦龂䖜濦滛濥垽㴈峾溵乑湚泿洕朄螾蟫噖嚚㖗噾䡛囙輑圁嶾湮㡥崟崯㞤訔㦩懚㥼愔廕粌㝙㪦冘裀禋䤺䲟淾銦鮣犾鈝㹞銀鈏㼉㧢斦慇㐆㧈檼垔䤃酳鷣栶檭猌㙬憖憗筃秵㣧䇙癮癊䪩㾙闉凐瘖訚誾婬婣飲侌㱃飮䌥絪緸讔䚿諲訡",
"ying": "应影英营映迎硬盈婴鹰颖赢荧蝇莹莺樱瑛萤鹦萦缨膺瀛荥璎嘤媵罂瘿茔楹郢滢颍嬴景蓥潆撄萾㲟鶧蘡藀䕦盁孾碤礯䃷朠膡䑉霙䨍珱瓔㼆䁐䀴鷪渶溁溋㵬浧㴄瀴濴瀠㶈瀯濙灐濚瀅蛍営鴬灜暎蝧蝿蠳蠅嚶甖巊鑍鸎罌嬰鸚賏譻巆愥煐㢍廮応罃褮塋䁝禜縈螢䪯營熒鶯覮鎣嫈瑩甇謍鶑噟應鷹譍䙬锳鐛鱦䤝㨕攖摬攍桜梬櫻櫿矨軈籝籯韺癭㿘媖孆偀僌㑞䭊䭗緓绬頴颕潁纓㯋穎贏",
"yo": "哟唷喲",
"yong": "用永勇拥涌庸泳佣咏雍踊蛹臃俑甬壅鳙恿痈邕喁慵湧墉镛饔苚蒏㦷勈硧砽惥埇䞻塎㙲慂滽㴩灉澭顒颙䗤踴嗈噰㞲嵱愑悀愹怺㶲醟鄘鷛廱彮㝘鲬鯒鱅鰫鏞郺擁柡栐㷏牅癰癕雝嫞傭㽫詠",
"you": "有又由优油友游右幼尤犹忧邮幽诱悠铀佑黝柚囿蚴酉釉疣猷莠攸祐鱿繇鼬蚰牖呦莜莸尢卣蝣宥铕侑苃䒴蕕䢊聈牰駀䀁鄾迶憂䳑肬貁䞥耰丣㻀逌䚃瀀沋㳺湵滺浟泑蜏䖻哊嚘㕱唀㘥輏輶㽕峟甴峳懮㤑怞怮庮麀䆜禉銪鲉鈾魷鮋䱂㹨狖㺠猶逰㮋栯櫌櫾酭梄槱楢郵怣牗㰶秞䅎䑻㕗羑㾞羪姷㚭優佦㒡㛜䬀偤纋孧㓜訧亴䛻遊誘䢟㫍",
"yu": "于与语育鱼余雨预域玉遇予欲宇愈渔誉郁羽狱御裕愉豫愚喻娱寓浴吁舆尉榆俞禹屿淤逾峪谕於迂虞瘀驭芋隅渝瑜阈毓盂汩熨禺腴揄臾煜钰彧鹬鬻谀馀聿纡竽伛龉觎圄欤妪玙邪蓣萸舁雩蜮昱蝓圉嵛庾庾燠窳窬饫狳瘐妤肀俣鹆蕷蘛㔱䩒芌蕍茰蒮䔡䖇萭薁蓹蘌茟匬萮陓隃䂊矞預鷸遹䮙驈馭䮇騟䂛戫礖砡㝼礇䃋硢硲䏸礜轝㦛鸒歟與譽輿䐳雤貐斞霱堣䨒迃亐圫䨞堬堉琙璵邘㺮㪀玗䢩敔䜽鳿瑀齬齵鸆䁌䲣䱷䁩睮歶淢㳚潏滪澦㳛盓澞湡漁灪淯㶛虶㬂䗨欥㬰蜟噳踰喅喩唹罭㽣輍骬嶼㠘髃嵎嶎䍞㠨崳惐䣁忬頨懙㥚㥥㤤㥔燏㡰粖庽㷒爩麌焴䢓㲾䴁䵫寙㝢䙔衧褕䆷穻鴧鴪䄏祤鈺鍝魣鱊鰅鴥鷠䰻魚鮽鯲㺞鐭䥏㺄獄銉鋊錥㼌扝扜挧魊扵棫櫲桙楰醧杅酑鬰欎欝鬱楀楡棛棜稢䈅稶穥籅䍂䄨䘘牏鄅㙑軉秗禦䉛篽籞艅艈籲込箊閾瘉羭癒䘱嫗嬩㚥䢖媀娛娯傴伃僪㒜儥兪覦歈㼶悆雓俁㑨㒁䬄偊饇飫餘螸慾鵒俼緎紆䋖㣃逳袬諛謣語斔䛕旟諭乻吾奥粥",
"yuan": "原员远元院源愿圆园缘援袁怨冤渊猿宛苑垣媛鸳辕沅爰橼塬鸢圜螈垸瑗鼋湲芫眢掾蒝薳䩩薗蒬茒葾鳶㹉䏍貟贠騵厡厵願鶢䳒䳣遠鼘逺邧䲮黿㤪盶溒渁鼝淵渆渕灁蝯蚖蜵蜎䖠蝝肙剈噮鶰員圎園轅囦圓㟶円㥳悁鹓惌鵷寃褑褤裫裷禐駌夗鴛妴鎱鈨魭鋺猨㭇榞榬杬酛棩櫞笎衏邍羱䅈䬇嫄媴嬽傆㥐䬧䬼䨊縁緣謜䛄䛇",
"yue": "月约越跃阅悦曰岳乐粤兑钥栎钺说刖瀹哕樾龠䖃戉蘥㹊玥䢲泧㬦蚏蚎䟠噦跀躍䠯啘䢁黦䡇軏岄曱嶽恱悅爚礿禴鉞鈅䥃鸑䤦鑰抈捳㰛籆矱籰粵籥篗箹閲閱嬳㜧妜㜰鸙䶳䋐約",
"yun": "运云允匀韵孕晕蕴芸陨酝韫耘恽纭熨愠氲筠郓郧殒员昀狁蕓䩵荺蕰薀蒀蒷蒕藴蘊阭耺隕馻夽奫磒腪䢵䨶䲰雲䞫霣㚃鋆殞齫齳眃沄澐涢溳蝹暈鄖䚋喗囩䵴䡝畇賱㟦㞌韗韞愪慍惲煴熉熅鄆運褞䆬鈗䤞勻抎氳抣枟橒醖醞秐䉙馧筼篔䦾䇖韻㾓㚺妘㛣㜏䪳伝傊餫紜緼缊縜縕贇赟",
"za": "杂咱扎咋砸咂匝拶䕹臜臢䞙䪞帀迊沯沞囋囃襍鉔魳桚韴雑雥雜",
"zai": "在再载灾仔栽宰崽哉甾䏁䮨載䵧烖㦲酨㱰睵渽溨洅㴓㞨賳扗畠䣬災傤儎縡",
"zan": "咱赞暂攒簪瓒錾糌趱昝㔆兂趲瓉鄼賛瓚濽灒噆喒暫蹔鏨㟛寁襸禶鐕鵤鐟撍攅攢揝橵贊簮㜺儧儹偺㤰饡㣅讃讚",
"zang": "藏脏葬赃臧奘驵蔵塟匨駔臟臓羘㘸贜贓髒賍賘弉銺牂",
"zao": "造早遭藻燥糟灶躁枣凿噪皂澡蚤唣薻䥣㲧趮栆璪璅䖣䗢蹧喿唕慥㷮煰鑿竃竈䲃皁醩棗梍簉艁䒃傮䜊譟",
"ze": "则责择泽侧啧仄赜咋昃帻箦迮舴蔶賾䕪䕉矠礋責齰䶦齚歵瞔㳻㳁澤溭沢滜泎汄蠌昗㖽嘖鸅幘則崱庂襗䰹皟捑擇択樍䇥簀㣱嫧諎謮",
"zei": "贼蠈賊戝鲗鱡鰂",
"zen": "怎谮䫈譖",
"zeng": "增综赠憎曾锃甑罾缯鬵磳増䰝璔囎㽪贈熷䙢鋥鱛橧矰鄫曽繒譄",
"zha": "扎炸眨渣闸喳榨诈栅札乍楂喋蚱柞铡咤查咋砟哳吒揸齄痄䕢䃎厏䞢耫霅㱜皻㪥㗬㴙溠䖳灹㡸宱觰鲊鍘鮓䥷抯摣紥挓搾拃柤醡樝皶蚻紮䵵牐齇劄箚䵙㷢閘鲝鮺偧㒀䋾譇䛽詐譗",
"zhai": "摘窄债宅寨斋翟砦责择侧祭齐瘵㡯鉙粂捚㩟榸檡夈債斎齋",
"zhan": "展战站占粘颤沾崭盏斩毡湛瞻栈辗詹绽蘸谵旃霑搌㠭菚虦盞䪌薝驏驙䩅氊趈䟋琖㻵虥䁴惉戦魙䗃蛅戰噡輾轏斬覱㟞岾嶃嶄嶦嶘㞡䎒䘺鳣䱠䱼鱣㺘棧桟醆枬榐栴橏䡀㣶閚嫸偡佔僝飐颭飦饘䋎綻詀讝氈鹯鸇邅譧譫旜",
"zhang": "长张章掌丈障涨帐仗胀账杖璋彰樟瘴漳蟑嶂鱆獐幛鄣嫜仉蔁騿礃脹墇㙣瞕涱漲暲㕩賬帳幥慞粻粀麞鏱扙痮㽴遧瘬傽餦張",
"zhao": "找照招召赵着兆昭沼诏朝钊肇濯啁棹罩爪嘲笊䮓駋㕚䃍㐍䝖爫趙垗瑵瞾曌㷖䍜羄燳㡽炤鍣釗鮡狣㺐鉊㨄櫂枛罀箌䈇䈃䍮㐒巶妱㑿佋皽肈肁旐詔",
"zhe": "这着者折哲遮浙蔗褶辙锗辄蛰蜇赭柘鹧摺螫谪著磔䩾䓆䎲㪿䮰䂞矺厇砓詟䐑䐲䏳喆嚞乽蟄謺䝕䝃歽淛蟅晣虴啫踷䠦嗻輙輒䵭轍㞏䗪鷓粍籷䊞襵袩銸鍺鮿埑晢啠悊㯰樜讋嫬這謫讁",
"zhei": "这",
"zhen": "真针阵镇振珍震诊侦贞枕圳砧斟疹臻甄祯桢朕赈帧榛缜箴畛稹填蓁胗溱浈轸鸩椹葴蒖䑐䫬薽萙塦陣聄㓄駗碪鬒䂧䂦㪛䏖䨯瑧殝珎遉貞眹眕㴨湞潧澵昣䟴辴轃黰甽軫賑帪幀䝩屒䲴寊䪴鴆裖袗禛禎鍼鋴針鎮錱覙鱵獉鉁鎭挋䳲揕搸抮㮳酙楨樼㯢栚籈姫嫃侲㐱偵弫䊶縥絼縝㣀眞紾紖纼誫診",
"zheng": "正政争整证征丁蒸症郑睁挣怔拯铮筝狰峥诤徵钲聇脀烝氶䂻鬇爭㱏埩靕鴊䥭睜眐塣晸踭䡕崢崝幁㡧㡠炡䥌鉦錚猙鏳掙揁掟抍撜愸篜箏徰䈣䦛䦶鄭㽀癥姃媜佂凧䋊䋫糽䛫証諍證",
"zhi": "之只制质知指直至志织支值致职止植置纸智执殖枝脂秩肢滞拓汁旨址稚芝吱帜蜘挚掷侄趾治识酯窒峙炙桎栉雉祗芷咫痣栀氏胝祇跖踯鸷蛭枳帙痔徵贽姪沚陟骘陟膣豸埴郅踬轾轵忮黹祉觯卮摭絷夂彘蘵芖䓌䛗䓜迣茋䓡藢䕌聀阯騭隲䏄㝂䎺職犆馶駤馽厔㕄砋礩䐭䐈䏯胑䑇乿膱墆鳷䧴坧䟈㙷覟墌疐坁垁漐縶贄慹騺鷙䥍摯執瓡驇臸瓆璏歭㫖淽滯滍汥洔淔洷㴛潌汦泜潪瀄晊蟙跱蹠躓躑㗌㗧㘉畤䡹輊軹豑豒剬䞃幟崻懥懫翐恉庤庢廌㡶熫寘衼襧衹袟禃祬祑帋觶觗䚦鋕銍铚䳅䱨鯯㩼锧鑕狾猘釞劧㨁貭搘挃㨖巵㧻抧摨搱扺扻劕質䭁擳擲梽榰㮹梔櫍椥柣櫛䵂栺樴㲛䝷鼅䵹鴙䅩秓徝稙憄䉅秷製䱥䄺䇛徏軄徴筫穉䆈稺秖䇽䉜㣥瘈痓䦯疻疷㜼娡㛿妷嬂値俧凪傂儨倁偫䬹隻綕緻䌤鴲紙紩織誌訨袠戠䫕旘",
"zhong": "中种重众终钟忠肿仲衷踵盅冢锺忪螽舯茽蔠刣尰鼨腫塚堹歱泈汷蚛蜙蹱喠眾幒煄炂衶衳祌銿鈡䱰鴤鍾狆㹣鐘㲴柊衆籦種㣫徸彸筗瘇妕媑妐偅伀㐺終螤諥蚣",
"zhou": "周州洲宙轴骤皱昼舟咒粥肘帚绉胄纣诌妯繇啁调荮碡酎籀䩜菷葤㔌䎻驟駎騆駲䐍䶇霌盩珘睭淍䖞晭嚋呪喌噣咮輖軸輈辀冑郮週㼙賙赒㥮粙炿烐皺鯞銂矪徟甃籒籕鸼鵃箒䈙䇠疛㾭晝婤㛩伷㑳㑇僽侜紂縐䋓诪譸詋䛆謅",
"zhu": "主住注助逐著宁筑诸珠猪竹朱柱祝驻株贮嘱煮铸烛蛛瞩竺蛀拄伫褚诛侏澍潴箸渚炷躅铢瘃苎术属茱翥洙麈橥杼槠邾舳疰丶茿莇藸蓫䕽苧蕏陼䎷逫馵䮱駯駐劯硃砫䐢墸䟉壴坾䬡煑䝒豬櫫矚眝㵭瀦濐灟乼蝫蠋曯蠾蠩跦跓囑鸀罜軴帾貯嵀䝬劚斸㤖㔉㫂燝燭爥炢麆㝉㿾䘢袾窋宔祩鋳鑄钃鯺鱁鮢䥮㺛銖㹥鉒拀㧣柷欘樦櫧笁篫築笜筯䍆鴸鼄篴䇧簗䇡秼㾻竚羜孎㑏佇䭖飳䰞䌵纻紵絑紸諸迬殶詝誅註",
"zhua": "抓爪髽膼撾檛簻挝",
"zhuai": "拽转跩",
"zhuan": "转专砖赚撰篆传颛馔啭蒃孨磚磗膞腞塼堟瑼鄟專甎叀専瑑蟤囀䡱転轉顓賺灷襈鱄篹籑䉵竱嫥僎饌縳諯譔",
"zhuang": "状装庄壮撞桩妆幢僮奘戆庒荘莊壵湷糚粧樁梉狀壯焋娤裝妝",
"zhui": "追缀椎坠锥赘惴骓隹缒墜騅硾礈腏膇贅沝畷䄌錣鑆鵻錐醊甀笍娷綴縋諈揣",
"zhun": "准谆淳屯肫窀埻迍準啍㡒宒衠稕凖綧訰諄",
"zhuo": "捉桌著卓着浊灼啄琢拙酌镯茁斫濯淖涿棹擢焯浞禚倬诼斮斲䕴䪼叕硺䶂龺圴斱琸鵫灂濁汋晫蠗啅罬斀劅㣿㪬蠿烵炪丵窡窧鐯鋜鐲㺟犳斵擆撯棳椓㭬槕櫡棁梲穱籗籱篧彴䅵穛娺妰諁諑謶鷟缴",
"zi": "自子资字紫仔姿滋兹姊籽咨孜渍梓髭恣滓谘淄呲孳鲻龇辎甾眦秭赀吱齐茈趑耔觜訾嵫锱笫粢缁芓蓻茡荢䔂茊葘菑茲孖牸矷頾頿胏䐉胾嗭赼趦鼒㺭剚鄑㱴㰷齜眥呰啙貲胔鈭㰣姕漬澬湽虸吇嗞輺輜崰䘣禌釨鰦鯔鎡镃鍿錙㧗杍橴榟椔秄䅆稵資栥秶㾅㜽姉鶅倳紎緇緕纃訿齍諮孶玆",
"zong": "总宗综纵踪棕粽鬃熜偬从腙葼蓗骔騌騣惣㹅鬉䰌碂磫朡堫䝋豵鬷昮蝬䗥蹤踨䍟嵏嵕嵸惾翪燪糭㷓糉㢔焧鑁鯮鯼鍐猔猣㚇揔摠搃捴㯶椶稯熧瘲疭倧傯倊綜緫緵總繌縦縱縂総緃",
"zou": "走奏邹揍陬鄹驺鲰诹菆郰棸騶赱㔿齱齺㵵䠫黀鄒鯫鯐掫棷箃緅諏",
"zu": "组族足祖阻租卒诅镞俎菹靻䔃蒩葅䯿珇䖕唨踤哫㞺崒崪䚝䱣鎺鏃爼椊䅸箤卆組䘚詛㲞㰵",
"zuan": "钻攥纂躜缵繤䂎躦鑚鉆鑽䤸劗籫纉纘䌣",
"zui": "最嘴罪醉咀蕞䮔厜璻蟕晬嗺噿嶵㠑嶊冣㝡䘹祽鋷錊酻酔樶檌㰎栬槜檇辠䘒稡纗絊",
"zun": "尊遵樽鳟撙墫噂嶟鶎銌鱒鐏捘罇鷷僔繜譐",
"zuo": "作做坐左座昨佐琢撮柞唑祚捽阼胙嘬怍酢笮葄葃蓙䔘苲莋㸲㝾䞰䎰咗㘀㘴岝岞䝫糳袏鈼㭮稓穝秨筰㛗㑅飵侳繓䋏"
}

View File

@@ -5,9 +5,9 @@ local BufferingIndicator = class(Element)
function BufferingIndicator:new() return Class.new(self) --[[@as BufferingIndicator]] end
function BufferingIndicator:init()
Element.init(self, 'buffer_indicator')
self.ignores_menu = true
Element.init(self, 'buffering_indicator', {ignores_curtain = true, render_order = 2})
self.enabled = false
self:decide_enabled()
end
function BufferingIndicator:decide_enabled()
@@ -15,7 +15,9 @@ function BufferingIndicator:decide_enabled()
local player = state.core_idle and not state.eof_reached
if self.enabled then
if not player or (state.pause and not cache) then self.enabled = false end
elseif player and cache and state.uncached_ranges then self.enabled = true end
elseif player and cache and state.uncached_ranges then
self.enabled = true
end
end
function BufferingIndicator:on_prop_pause() self:decide_enabled() end
@@ -27,9 +29,9 @@ function BufferingIndicator:on_prop_cache_underrun() self:decide_enabled() end
function BufferingIndicator:render()
local ass = assdraw.ass_new()
ass:rect(0, 0, display.width, display.height, {color = bg, opacity = 0.3})
ass:rect(0, 0, display.width, display.height, {color = bg, opacity = config.opacity.buffering_indicator})
local size = round(30 + math.min(display.width, display.height) / 10)
local opacity = (Elements.menu and not Elements.menu.is_closing) and 0.3 or 0.8
local opacity = (Elements.menu and Elements.menu:is_alive()) and 0.3 or 0.8
ass:spinner(display.width / 2, display.height / 2, size, {color = fg, opacity = opacity})
return ass
end

View File

@@ -1,6 +1,6 @@
local Element = require('elements/Element')
---@alias ButtonProps {icon: string; on_click: function; anchor_id?: string; active?: boolean; badge?: string|number; foreground?: string; background?: string; tooltip?: string}
---@alias ButtonProps {icon: string; on_click?: function; is_clickable?: boolean; anchor_id?: string; active?: boolean; badge?: string|number; foreground?: string; background?: string; tooltip?: string}
---@class Button : Element
local Button = class(Element)
@@ -17,13 +17,15 @@ function Button:init(id, props)
self.badge = props.badge
self.foreground = props.foreground or fg
self.background = props.background or bg
---@type fun()
self.is_clickable = true
---@type fun()|nil
self.on_click = props.on_click
Element.init(self, id, props)
end
function Button:on_coordinates() self.font_size = round((self.by - self.ay) * 0.7) end
function Button:handle_cursor_down()
function Button:handle_cursor_click()
if not self.on_click or not self.is_clickable then return end
-- We delay the callback to next tick, otherwise we are risking race
-- conditions as we are in the middle of event dispatching.
-- For example, handler might add a menu to the end of the element stack, and that
@@ -34,21 +36,23 @@ end
function Button:render()
local visibility = self:get_visibility()
if visibility <= 0 then return end
if self.proximity_raw == 0 then
cursor.on_primary_down = function() self:handle_cursor_down() end
end
cursor:zone('primary_click', self, function() self:handle_cursor_click() end)
local ass = assdraw.ass_new()
local is_clickable = self.is_clickable and self.on_click ~= nil
local is_hover = self.proximity_raw == 0
local is_hover_or_active = is_hover or self.active
local foreground = self.active and self.background or self.foreground
local background = self.active and self.foreground or self.background
local background_opacity = self.active and 1 or config.opacity.controls
if is_hover and is_clickable and background_opacity < 0.3 then background_opacity = 0.3 end
-- Background
if is_hover_or_active then
if background_opacity > 0 then
ass:rect(self.ax, self.ay, self.bx, self.by, {
color = self.active and background or foreground, radius = 2,
opacity = visibility * (self.active and 1 or 0.3),
color = (self.active or not is_hover) and background or foreground,
radius = state.radius,
opacity = visibility * background_opacity,
})
end
@@ -64,8 +68,11 @@ function Button:render()
local width, height = math.ceil(badge_width + (badge_font_size / 7) * 2), math.ceil(badge_font_size * 0.93)
local bx, by = self.bx - 1, self.by - 1
ass:rect(bx - width, by - height, bx, by, {
color = foreground, radius = 2, opacity = visibility,
border = self.active and 0 or 1, border_color = background,
color = foreground,
radius = state.radius,
opacity = visibility,
border = self.active and 0 or 1,
border_color = background,
})
ass:txt(bx - width / 2, by - height / 2, 5, self.badge, badge_opts)
@@ -80,8 +87,11 @@ function Button:render()
-- Icon
local x, y = round(self.ax + (self.bx - self.ax) / 2), round(self.ay + (self.by - self.ay) / 2)
ass:icon(x, y, self.font_size, self.icon, {
color = foreground, border = self.active and 0 or options.text_border, border_color = background,
opacity = visibility, clip = icon_clip,
color = foreground,
border = self.active and 0 or options.text_border * state.scale,
border_color = background,
opacity = visibility,
clip = icon_clip,
})
return ass

View File

@@ -1,45 +1,61 @@
local Element = require('elements/Element')
local Button = require('elements/Button')
local CycleButton = require('elements/CycleButton')
local ManagedButton = require('elements/ManagedButton')
local Speed = require('elements/Speed')
-- `scale` - `options.controls_size` scale factor.
-- `ratio` - Width/height ratio of a static or dynamic element.
-- `ratio_min` Min ratio for 'dynamic' sized element.
---@alias ControlItem {element?: Element; kind: string; sizing: 'space' | 'static' | 'dynamic'; scale: number; ratio?: number; ratio_min?: number; hide: boolean; dispositions?: table<string, boolean>}
-- sizing:
-- static - shrink, have highest claim on available space, disappear when there's not enough of it
-- dynamic - shrink to make room for static elements until they reach their ratio_min, then disappear
-- gap - shrink if there's no space left
-- space - expands to fill available space, shrinks as needed
-- scale - `options.controls_size` scale factor.
-- ratio - Width/height ratio of a static or dynamic element.
-- ratio_min Min ratio for 'dynamic' sized element.
---@alias ControlItem {element?: Element; kind: string; sizing: 'space' | 'static' | 'dynamic' | 'gap'; scale: number; ratio?: number; ratio_min?: number; hide: boolean; dispositions?: table<string, boolean>}
---@class Controls : Element
local Controls = class(Element)
function Controls:new() return Class.new(self) --[[@as Controls]] end
function Controls:init()
Element.init(self, 'controls')
Element.init(self, 'controls', {render_order = 6})
---@type ControlItem[] All control elements serialized from `options.controls`.
self.controls = {}
---@type ControlItem[] Only controls that match current dispositions.
self.layout = {}
self:init_options()
end
function Controls:destroy()
self:destroy_elements()
Element.destroy(self)
end
function Controls:init_options()
-- Serialize control elements
local shorthands = {
menu = 'command:menu:script-binding uosc/menu-blurred?Menu',
subtitles = 'command:subtitles:script-binding uosc/subtitles#sub>0?Subtitles',
audio = 'command:graphic_eq:script-binding uosc/audio#audio>1?Audio',
['audio-device'] = 'command:speaker:script-binding uosc/audio-device?Audio device',
video = 'command:theaters:script-binding uosc/video#video>1?Video',
playlist = 'command:list_alt:script-binding uosc/playlist?Playlist',
chapters = 'command:bookmark:script-binding uosc/chapters#chapters>0?Chapters',
['editions'] = 'command:bookmarks:script-binding uosc/editions#editions>1?Editions',
['stream-quality'] = 'command:high_quality:script-binding uosc/stream-quality?Stream quality',
['open-file'] = 'command:file_open:script-binding uosc/open-file?Open file',
['items'] = 'command:list_alt:script-binding uosc/items?Playlist/Files',
prev = 'command:arrow_back_ios:script-binding uosc/prev?Previous',
next = 'command:arrow_forward_ios:script-binding uosc/next?Next',
first = 'command:first_page:script-binding uosc/first?First',
last = 'command:last_page:script-binding uosc/last?Last',
['loop-playlist'] = 'cycle:repeat:loop-playlist:no/inf!?Loop playlist',
['loop-file'] = 'cycle:repeat_one:loop-file:no/inf!?Loop file',
shuffle = 'toggle:shuffle:shuffle?Shuffle',
fullscreen = 'cycle:crop_free:fullscreen:no/yes=fullscreen_exit!?Fullscreen',
['play-pause'] = 'cycle:pause:pause:no/yes=play_arrow?' .. t('Play/Pause'),
menu = 'command:menu:script-binding uosc/menu-blurred?' .. t('Menu'),
subtitles = 'command:subtitles:script-binding uosc/subtitles#sub>0?' .. t('Subtitles'),
audio = 'command:graphic_eq:script-binding uosc/audio#audio>1?' .. t('Audio'),
['audio-device'] = 'command:speaker:script-binding uosc/audio-device?' .. t('Audio device'),
video = 'command:theaters:script-binding uosc/video#video>1?' .. t('Video'),
playlist = 'command:list_alt:script-binding uosc/playlist?' .. t('Playlist'),
chapters = 'command:bookmark:script-binding uosc/chapters#chapters>0?' .. t('Chapters'),
['editions'] = 'command:bookmarks:script-binding uosc/editions#editions>1?' .. t('Editions'),
['stream-quality'] = 'command:high_quality:script-binding uosc/stream-quality?' .. t('Stream quality'),
['open-file'] = 'command:file_open:script-binding uosc/open-file?' .. t('Open file'),
['items'] = 'command:list_alt:script-binding uosc/items?' .. t('Playlist/Files'),
prev = 'command:arrow_back_ios:script-binding uosc/prev?' .. t('Previous'),
next = 'command:arrow_forward_ios:script-binding uosc/next?' .. t('Next'),
first = 'command:first_page:script-binding uosc/first?' .. t('First'),
last = 'command:last_page:script-binding uosc/last?' .. t('Last'),
['loop-playlist'] = 'cycle:repeat:loop-playlist:no/inf!?' .. t('Loop playlist'),
['loop-file'] = 'cycle:repeat_one:loop-file:no/inf!?' .. t('Loop file'),
shuffle = 'toggle:shuffle:shuffle?' .. t('Shuffle'),
fullscreen = 'cycle:crop_free:fullscreen:no/yes=fullscreen_exit!?' .. t('Fullscreen'),
}
-- Parse out disposition/config pairs
@@ -48,8 +64,10 @@ function Controls:init()
local current_item = nil
for c in options.controls:gmatch('.') do
if not current_item then current_item = {disposition = '', config = ''} end
if c == '<' and #current_item.config == 0 then in_disposition = true
elseif c == '>' and #current_item.config == 0 then in_disposition = false
if c == '<' and #current_item.config == 0 then
in_disposition = true
elseif c == '>' and #current_item.config == 0 then
in_disposition = false
elseif c == ',' and not in_disposition then
items[#items + 1] = current_item
current_item = nil
@@ -65,7 +83,7 @@ function Controls:init()
for i, item in ipairs(items) do
local config = shorthands[item.config] and shorthands[item.config] or item.config
local config_tooltip = split(config, ' *%? *')
local tooltip = t(config_tooltip[2])
local tooltip = config_tooltip[2]
config = shorthands[config_tooltip[1]]
and split(shorthands[config_tooltip[1]], ' *%? *')[1] or config_tooltip[1]
local config_badge = split(config, ' *# *')
@@ -76,7 +94,7 @@ function Controls:init()
-- Serialize dispositions
local dispositions = {}
for _, definition in ipairs(split(item.disposition, ' *, *')) do
for _, definition in ipairs(comma_split(item.disposition)) do
if #definition > 0 then
local value = definition:sub(1, 1) ~= '!'
local name = not value and definition:sub(2) or definition
@@ -97,7 +115,7 @@ function Controls:init()
if kind == 'space' then
control.sizing = 'space'
elseif kind == 'gap' then
table_assign(control, {sizing = 'dynamic', scale = 1, ratio = params[1] or 0.3, ratio_min = 0})
table_assign(control, {sizing = 'gap', scale = 1, ratio = params[1] or 0.3, ratio_min = 0})
elseif kind == 'command' then
if #params ~= 2 then
mp.error(string.format(
@@ -105,6 +123,7 @@ function Controls:init()
))
else
local element = Button:new('control_' .. i, {
render_order = self.render_order,
icon = params[1],
anchor_id = 'controls',
on_click = function() mp.command(params[2]) end,
@@ -136,16 +155,34 @@ function Controls:init()
end
local element = CycleButton:new('control_' .. i, {
prop = params[2], anchor_id = 'controls', states = states, tooltip = tooltip,
render_order = self.render_order,
prop = params[2],
anchor_id = 'controls',
states = states,
tooltip = tooltip,
})
table_assign(control, {element = element, sizing = 'static', scale = 1, ratio = 1})
if badge then self:register_badge_updater(badge, element) end
end
elseif kind == 'button' then
if #params ~= 1 then
mp.error(string.format(
'managed button needs 1 parameter, %d received: %s', #params, table.concat(params, '/')
))
else
local element = ManagedButton:new('control_' .. i, {
name = params[1],
render_order = self.render_order,
anchor_id = 'controls',
})
table_assign(control, {element = element, sizing = 'static', scale = 1, ratio = 1})
end
elseif kind == 'speed' then
if not Elements.speed then
local element = Speed:new({anchor_id = 'controls'})
local element = Speed:new({anchor_id = 'controls', render_order = self.render_order})
local scale = tonumber(params[1]) or 1.3
table_assign(control, {
element = element, sizing = 'dynamic', scale = params[1] or 1.3, ratio = 3.5, ratio_min = 2,
element = element, sizing = 'dynamic', scale = scale, ratio = 3.5, ratio_min = 2,
})
else
msg.error('there can only be 1 speed slider')
@@ -209,25 +246,27 @@ function Controls:register_badge_updater(badge, element)
request_render()
end
if is_external_prop then element['on_external_prop_' .. prop] = function(_, value) handler(prop, value) end
else mp.observe_property(observable_name, 'native', handler) end
if is_external_prop then
element['on_external_prop_' .. prop] = function(_, value) handler(prop, value) end
else
self:observe_mp_property(observable_name, handler)
end
end
function Controls:get_visibility()
return (Elements.speed and Elements.speed.dragging) and 1 or Elements.timeline:get_is_hovered()
return Elements:v('speed', 'dragging') and 1 or Elements:maybe('timeline', 'get_is_hovered')
and -1 or Element.get_visibility(self)
end
function Controls:update_dimensions()
local window_border = Elements.window_border.size
local size = state.fullormaxed and options.controls_size_fullscreen or options.controls_size
local spacing = options.controls_spacing
local margin = options.controls_margin
local window_border = Elements:v('window_border', 'size', 0)
local size = round(options.controls_size * state.scale)
local spacing = round(options.controls_spacing * state.scale)
local margin = round(options.controls_margin * state.scale)
-- Disable when not enough space
local available_space = display.height - Elements.window_border.size * 2
if Elements.top_bar.enabled then available_space = available_space - Elements.top_bar.size end
if Elements.timeline.enabled then available_space = available_space - Elements.timeline.size_max end
local available_space = display.height - window_border * 2 - Elements:v('top_bar', 'size', 0)
- Elements:v('timeline', 'size', 0)
self.enabled = available_space > size + 10
-- Reset hide/enabled flags
@@ -240,25 +279,28 @@ function Controls:update_dimensions()
-- Container
self.bx = display.width - window_border - margin
self.by = (Elements.timeline.enabled and Elements.timeline.ay or display.height - window_border) - margin
self.by = Elements:v('timeline', 'ay', display.height - window_border) - margin
self.ax, self.ay = window_border + margin, self.by - size
-- Controls
local available_width = self.bx - self.ax
local statics_width = (#self.layout - 1) * spacing
local available_width, statics_width = self.bx - self.ax, 0
local min_content_width = statics_width
local max_dynamics_width, dynamic_units, spaces = 0, 0, 0
local max_dynamics_width, dynamic_units, spaces, gaps = 0, 0, 0, 0
-- Calculate statics_width, min_content_width, and count spaces
-- Calculate statics_width, min_content_width, and count spaces & gaps
for c, control in ipairs(self.layout) do
if control.sizing == 'space' then
spaces = spaces + 1
elseif control.sizing == 'gap' then
gaps = gaps + control.scale * control.ratio
elseif control.sizing == 'static' then
local width = size * control.scale * control.ratio
local width = size * control.scale * control.ratio + (c ~= #self.layout and spacing or 0)
statics_width = statics_width + width
min_content_width = min_content_width + width
elseif control.sizing == 'dynamic' then
min_content_width = min_content_width + size * control.scale * control.ratio_min
local spacing = (c ~= #self.layout and spacing or 0)
statics_width = statics_width + spacing
min_content_width = min_content_width + size * control.scale * control.ratio_min + spacing
max_dynamics_width = max_dynamics_width + size * control.scale * control.ratio
dynamic_units = dynamic_units + control.scale * control.ratio
end
@@ -271,7 +313,7 @@ function Controls:update_dimensions()
i = i + (a * (a % 2 == 0 and 1 or -1))
local control = self.layout[i]
if control.kind ~= 'gap' and control.kind ~= 'space' then
if control.sizing ~= 'gap' and control.sizing ~= 'space' then
control.hide = true
if control.element then control.element.enabled = false end
if control.sizing == 'static' then
@@ -279,6 +321,7 @@ function Controls:update_dimensions()
min_content_width = min_content_width - width - spacing
statics_width = statics_width - width - spacing
elseif control.sizing == 'dynamic' then
statics_width = statics_width - spacing
min_content_width = min_content_width - size * control.scale * control.ratio_min - spacing
max_dynamics_width = max_dynamics_width - size * control.scale * control.ratio
dynamic_units = dynamic_units - control.scale * control.ratio
@@ -292,7 +335,9 @@ function Controls:update_dimensions()
-- Lay out the elements
local current_x = self.ax
local width_for_dynamics = available_width - statics_width
local space_width = (width_for_dynamics - max_dynamics_width) / spaces
local empty_space_width = width_for_dynamics - max_dynamics_width
local width_for_gaps = math.min(empty_space_width, size * gaps)
local individual_space_width = spaces > 0 and ((empty_space_width - width_for_gaps) / spaces) or 0
for c, control in ipairs(self.layout) do
if not control.hide then
@@ -300,7 +345,9 @@ function Controls:update_dimensions()
local width, height = 0, 0
if sizing == 'space' then
if space_width > 0 then width = space_width end
if individual_space_width > 0 then width = individual_space_width end
elseif sizing == 'gap' then
if width_for_gaps > 0 then width = width_for_gaps * (ratio / gaps) end
elseif sizing == 'static' then
height = size * scale
width = height * ratio
@@ -312,7 +359,7 @@ function Controls:update_dimensions()
local bx = current_x + width
if element then element:set_coordinates(round(current_x), round(self.by - height), bx, self.by) end
current_x = bx + spacing
current_x = element and bx + spacing or bx
end
end
@@ -323,7 +370,19 @@ end
function Controls:on_dispositions() self:reflow() end
function Controls:on_display() self:update_dimensions() end
function Controls:on_prop_border() self:update_dimensions() end
function Controls:on_prop_title_bar() self:update_dimensions() end
function Controls:on_prop_fullormaxed() self:update_dimensions() end
function Controls:on_timeline_enabled() self:update_dimensions() end
function Controls:destroy_elements()
for _, control in ipairs(self.controls) do
if control.element then control.element:destroy() end
end
end
function Controls:on_options()
self:destroy_elements()
self:init_options()
end
return Controls

View File

@@ -5,7 +5,7 @@ local Curtain = class(Element)
function Curtain:new() return Class.new(self) --[[@as Curtain]] end
function Curtain:init()
Element.init(self, 'curtain', {ignores_menu = true})
Element.init(self, 'curtain', {render_order = 999})
self.opacity = 0
---@type string[]
self.dependents = {}
@@ -24,10 +24,10 @@ function Curtain:unregister(id)
end
function Curtain:render()
if self.opacity == 0 or options.curtain_opacity == 0 then return end
if self.opacity == 0 or config.opacity.curtain == 0 then return end
local ass = assdraw.ass_new()
ass:rect(0, 0, display.width, display.height, {
color = '000000', opacity = options.curtain_opacity * self.opacity,
color = config.color.curtain, opacity = config.opacity.curtain * self.opacity,
})
return ass
end

View File

@@ -34,8 +34,14 @@ function CycleButton:init(id, props)
end
end
self.handle_change = function(name, value)
if is_state_prop and type(value) == 'boolean' then value = value and 'yes' or 'no' end
local function handle_change(name, value)
-- Removes unnecessary floating point digits from values like `2.00000`.
-- This happens when observing properties like `speed`.
if type(value) == 'string' and string.match(value, '^[%+%-]?%d+%.%d+$') then
value = tonumber(value)
end
value = type(value) == 'boolean' and (value and 'yes' or 'no') or tostring(value or '')
local index = itable_find(self.states, function(state) return state.value == value end)
self.current_state_index = index or 1
self.icon = self.states[self.current_state_index].icon
@@ -46,19 +52,14 @@ function CycleButton:init(id, props)
local prop_parts = split(self.prop, '@')
if #prop_parts == 2 then -- External prop with a script owner
self.prop, self.owner = prop_parts[1], prop_parts[2]
self['on_external_prop_' .. self.prop] = function(_, value) self.handle_change(self.prop, value) end
self.handle_change(self.prop, external[self.prop])
self['on_external_prop_' .. self.prop] = function(_, value) handle_change(self.prop, value) end
handle_change(self.prop, external[self.prop])
elseif is_state_prop then -- uosc's state props
self['on_prop_' .. self.prop] = function(self, value) self.handle_change(self.prop, value) end
self.handle_change(self.prop, state[self.prop])
self['on_prop_' .. self.prop] = function(self, value) handle_change(self.prop, value) end
handle_change(self.prop, state[self.prop])
else
mp.observe_property(self.prop, 'string', self.handle_change)
self:observe_mp_property(self.prop, 'string', handle_change)
end
end
function CycleButton:destroy()
Button.destroy(self)
mp.unobserve_property(self.handle_change)
end
return CycleButton

View File

@@ -1,4 +1,4 @@
---@alias ElementProps {enabled?: boolean; ax?: number; ay?: number; bx?: number; by?: number; ignores_menu?: boolean; anchor_id?: string;}
---@alias ElementProps {enabled?: boolean; render_order?: number; ax?: number; ay?: number; bx?: number; by?: number; ignores_curtain?: boolean; anchor_id?: string;}
-- Base class all elements inherit from.
---@class Element : Class
@@ -8,6 +8,7 @@ local Element = class()
---@param props? ElementProps
function Element:init(id, props)
self.id = id
self.render_order = 1
-- `false` means element won't be rendered, or receive events
self.enabled = true
-- Element coordinates
@@ -15,15 +16,17 @@ function Element:init(id, props)
-- Relative proximity from `0` - mouse outside `proximity_max` range, to `1` - mouse within `proximity_min` range.
self.proximity = 0
-- Raw proximity in pixels.
self.proximity_raw = INFINITY
self.proximity_raw = math.huge
---@type number `0-1` factor to force min visibility. Used for toggling element's permanent visibility.
self.min_visibility = 0
---@type number `0-1` factor to force a visibility value. Used for flashing, fading out, and other animations
self.forced_visibility = nil
---@type boolean Render this element even when menu is open.
self.ignores_menu = false
---@type boolean Show this element even when curtain is visible.
self.ignores_curtain = false
---@type nil|string ID of an element from which this one should inherit visibility.
self.anchor_id = nil
---@type fun()[] Disposer functions called when element is destroyed.
self._disposers = {}
if props then table_assign(self, props) end
@@ -31,8 +34,11 @@ function Element:init(id, props)
self._flash_out_timer = mp.add_timeout(options.flash_duration / 1000, function()
local function getTo() return self.proximity end
local function onTweenEnd() self.forced_visibility = nil end
if self.enabled then self:tween_property('forced_visibility', 1, getTo, onTweenEnd)
else onTweenEnd() end
if self.enabled then
self:tween_property('forced_visibility', self:get_visibility(), getTo, onTweenEnd)
else
onTweenEnd()
end
end)
self._flash_out_timer:kill()
@@ -40,11 +46,12 @@ function Element:init(id, props)
end
function Element:destroy()
for _, disposer in ipairs(self._disposers) do disposer() end
self.destroyed = true
Elements:remove(self)
end
function Element:reset_proximity() self.proximity, self.proximity_raw = 0, INFINITY end
function Element:reset_proximity() self.proximity, self.proximity_raw = 0, math.huge end
---@param ax number
---@param ay number
@@ -70,18 +77,23 @@ function Element:is_persistent()
local persist = config[self.id .. '_persistency']
return persist and (
(persist.audio and state.is_audio)
or (persist.paused and state.pause and (not Elements.timeline.pressed or Elements.timeline.pressed.pause))
or (
persist.paused and state.pause
and (not Elements.timeline or not Elements.timeline.pressed or Elements.timeline.pressed.pause)
)
or (persist.video and state.is_video)
or (persist.image and state.is_image)
or (persist.idle and state.is_idle)
or (persist.windowed and not state.fullormaxed)
or (persist.fullscreen and state.fullormaxed)
)
end
-- Decide elements visibility based on proximity and various other factors
function Element:get_visibility()
-- Hide when menu is open, unless this is a menu
---@diagnostic disable-next-line: undefined-global
if not self.ignores_menu and Menu and Menu:is_open() then return 0 end
-- Hide when curtain is visible, unless this elements ignores it
local min_order = (Elements.curtain.opacity > 0 and not self.ignores_curtain) and Elements.curtain.render_order or 0
if self.render_order < min_order then return 0 end
-- Persistency
if self:is_persistent() then return 1 end
@@ -106,12 +118,12 @@ end
---@param from number
---@param to number|fun():number
---@param setter fun(value: number)
---@param factor_or_callback? number|fun()
---@param duration_or_callback? number|fun() Duration in milliseconds or a callback function.
---@param callback? fun() Called either on animation end, or when animation is killed.
function Element:tween(from, to, setter, factor_or_callback, callback)
function Element:tween(from, to, setter, duration_or_callback, callback)
self:tween_stop()
self._kill_tween = self.enabled and tween(
from, to, setter, factor_or_callback,
from, to, setter, duration_or_callback,
function()
self._kill_tween = nil
if callback then callback() end
@@ -126,10 +138,10 @@ function Element:tween_stop() self:maybe('_kill_tween') end
---@param prop string
---@param from number
---@param to number|fun():number
---@param factor_or_callback? number|fun()
---@param duration_or_callback? number|fun() Duration in milliseconds or a callback function.
---@param callback? fun() Called either on animation end, or when animation is killed.
function Element:tween_property(prop, from, to, factor_or_callback, callback)
self:tween(from, to, function(value) self[prop] = value end, factor_or_callback, callback)
function Element:tween_property(prop, from, to, duration_or_callback, callback)
self:tween(from, to, function(value) self[prop] = value end, duration_or_callback, callback)
end
---@param name string
@@ -146,9 +158,37 @@ function Element:flash()
self:tween_stop()
self.forced_visibility = 1
request_render()
self._flash_out_timer.timeout = options.flash_duration / 1000
self._flash_out_timer:kill()
self._flash_out_timer:resume()
end
end
-- Register disposer to be called when element is destroyed.
---@param disposer fun()
function Element:register_disposer(disposer)
if not itable_index_of(self._disposers, disposer) then
self._disposers[#self._disposers + 1] = disposer
end
end
-- Automatically registers disposer for the passed callback.
---@param event string
---@param callback fun()
function Element:register_mp_event(event, callback)
mp.register_event(event, callback)
self:register_disposer(function() mp.unregister_event(callback) end)
end
-- Automatically registers disposer for the observer.
---@param name string
---@param type_or_callback string|fun(name: string, value: any)
---@param callback_maybe nil|fun(name: string, value: any)
function Element:observe_mp_property(name, type_or_callback, callback_maybe)
local callback = type(type_or_callback) == 'function' and type_or_callback or callback_maybe
local prop_type = type(type_or_callback) == 'string' and type_or_callback or 'native'
mp.observe_property(name, prop_type, callback)
self:register_disposer(function() mp.unobserve_property(callback) end)
end
return Element

View File

@@ -1,4 +1,4 @@
local Elements = {itable = {}}
local Elements = {_all = {}}
---@param element Element
function Elements:add(element)
@@ -9,9 +9,12 @@ function Elements:add(element)
if self:has(element.id) then Elements:remove(element.id) end
self.itable[#self.itable + 1] = element
self._all[#self._all + 1] = element
self[element.id] = element
-- Sort by render order
table.sort(self._all, function(a, b) return a.render_order < b.render_order end)
request_render()
end
@@ -22,26 +25,25 @@ function Elements:remove(idOrElement)
if element then
if not element.destroyed then element:destroy() end
element.enabled = false
self.itable = itable_delete_value(self.itable, self[id])
self._all = itable_delete_value(self._all, self[id])
self[id] = nil
request_render()
end
end
function Elements:update_proximities()
local menu_only = Elements.menu ~= nil
local curtain_render_order = Elements.curtain.opacity > 0 and Elements.curtain.render_order or 0
local mouse_leave_elements = {}
local mouse_enter_elements = {}
-- Calculates proximities and opacities for defined elements
-- Calculates proximities for all elements
for _, element in self:ipairs() do
if element.enabled then
local previous_proximity_raw = element.proximity_raw
-- If menu is open, all other elements have to be disabled
if menu_only then
if element.ignores_menu then element:update_proximity()
else element:reset_proximity() end
-- If curtain is open, we disable all elements set to rendered below it
if not element.ignores_curtain and element.render_order < curtain_render_order then
element:reset_proximity()
else
element:update_proximity()
end
@@ -68,8 +70,12 @@ end
-- Toggles passed elements' min visibilities between 0 and 1.
---@param ids string[] IDs of elements to peek.
function Elements:toggle(ids)
local has_invisible = itable_find(ids, function(id) return Elements[id] and Elements[id]:get_visibility() ~= 1 end)
local has_invisible = itable_find(ids, function(id)
return Elements[id] and Elements[id].enabled and Elements[id]:get_visibility() ~= 1
end)
self:set_min_visibility(has_invisible and 1 or 0, ids)
-- Reset proximities when toggling off. Has to happen after `set_min_visibility`,
-- as that is using proximity as a tween starting point.
if not has_invisible then
@@ -95,8 +101,13 @@ end
-- Flash passed elements.
---@param ids string[] IDs of elements to peek.
function Elements:flash(ids)
local elements = itable_filter(self.itable, function(element) return itable_index_of(ids, element.id) ~= nil end)
local elements = itable_filter(self._all, function(element) return itable_has(ids, element.id) end)
for _, element in ipairs(elements) do element:flash() end
-- Special case for 'progress' since it's a state of timeline, not an element
if itable_has(ids, 'progress') and not itable_has(ids, 'timeline') then
Elements:maybe('timeline', 'flash_progress')
end
end
---@param name string Event name.
@@ -108,8 +119,8 @@ end
-- Disabled elements don't receive these events.
---@param name string Event name.
function Elements:proximity_trigger(name, ...)
for i = #self.itable, 1, -1 do
local element = self.itable[i]
for i = #self._all, 1, -1 do
local element = self._all[i]
if element.enabled then
if element.proximity_raw == 0 then
if element:trigger(name, ...) == 'stop_propagation' then break end
@@ -119,7 +130,23 @@ function Elements:proximity_trigger(name, ...)
end
end
-- Returns a property of an element with a passed `id` if it exists, with an optional fallback.
---@param id string
---@param prop string
---@param fallback any
function Elements:v(id, prop, fallback)
if self[id] and self[id].enabled and self[id][prop] ~= nil then return self[id][prop] end
return fallback
end
-- Calls a method on an element with passed `id` if it exists.
---@param id string
---@param method string
function Elements:maybe(id, method, ...)
if self[id] then return self[id]:maybe(method, ...) end
end
function Elements:has(id) return self[id] ~= nil end
function Elements:ipairs() return ipairs(self.itable) end
function Elements:ipairs() return ipairs(self._all) end
return Elements

View File

@@ -0,0 +1,29 @@
local Button = require('elements/Button')
---@alias ManagedButtonProps {name: string; anchor_id?: string; render_order?: number}
---@class ManagedButton : Button
local ManagedButton = class(Button)
---@param id string
---@param props ManagedButtonProps
function ManagedButton:new(id, props) return Class.new(self, id, props) --[[@as ManagedButton]] end
---@param id string
---@param props ManagedButtonProps
function ManagedButton:init(id, props)
---@type string | table | nil
self.command = nil
Button.init(self, id, table_assign({}, props, {on_click = function() execute_command(self.command) end}))
self:register_disposer(buttons:subscribe(props.name, function(data) self:update(data) end))
end
function ManagedButton:update(data)
for _, prop in ipairs({'icon', 'active', 'badge', 'command', 'tooltip'}) do
self[prop] = data[prop]
end
self.is_clickable = self.command ~= nil
end
return ManagedButton

1543
src/uosc/elements/Menu.lua Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -5,43 +5,33 @@ local PauseIndicator = class(Element)
function PauseIndicator:new() return Class.new(self) --[[@as PauseIndicator]] end
function PauseIndicator:init()
Element.init(self, 'pause_indicator')
self.ignores_menu = true
self.base_icon_opacity = options.pause_indicator == 'flash' and 1 or 0.8
Element.init(self, 'pause_indicator', {render_order = 3})
self.ignores_curtain = true
self.paused = state.pause
self.type = options.pause_indicator
self.is_manual = options.pause_indicator == 'manual'
self.fadeout_requested = false
self.opacity = 0
self.fadeout = false
self:init_options()
end
mp.observe_property('pause', 'bool', function(_, paused)
if Elements.timeline.pressed then return end
if options.pause_indicator == 'flash' then
if self.paused == paused then return end
self:flash()
elseif options.pause_indicator == 'static' then
self:decide()
end
end)
function PauseIndicator:init_options()
self.base_icon_opacity = options.pause_indicator == 'flash' and 1 or 0.8
self.type = options.pause_indicator
self:on_prop_pause()
end
function PauseIndicator:flash()
if not self.is_manual and self.type ~= 'flash' then return end
-- can't wait for pause property event listener to set this, because when this is used inside a binding like:
-- Can't wait for pause property event listener to set this, because when this is used inside a binding like:
-- cycle pause; script-binding uosc/flash-pause-indicator
-- the pause event is not fired fast enough, and indicator starts rendering with old icon
-- The pause event is not fired fast enough, and indicator starts rendering with old icon.
self.paused = mp.get_property_native('pause')
if self.is_manual then self.type = 'flash' end
self.opacity = 1
self:tween_property('opacity', 1, 0, 0.15)
self.fadeout, self.opacity = false, 1
self:tween_property('opacity', 1, 0, 300)
end
-- decides whether static indicator should be visible or not
-- Decides whether static indicator should be visible or not.
function PauseIndicator:decide()
if not self.is_manual and self.type ~= 'static' then return end
self.paused = mp.get_property_native('pause') -- see flash() for why this line is necessary
if self.is_manual then self.type = 'static' end
self.opacity = self.paused and 1 or 0
self.fadeout, self.opacity = self.paused, self.paused and 1 or 0
request_render()
-- Workaround for an mpv race condition bug during pause on windows builds, which causes osd updates to be ignored.
@@ -49,19 +39,32 @@ function PauseIndicator:decide()
mp.add_timeout(.05, function() osd:update() end)
end
function PauseIndicator:on_prop_pause()
if Elements:v('timeline', 'pressed') then return end
if options.pause_indicator == 'flash' then
if self.paused ~= state.pause then self:flash() end
elseif options.pause_indicator == 'static' then
self:decide()
end
end
function PauseIndicator:on_options()
self:init_options()
if self.type == 'flash' then self.opacity = 0 end
end
function PauseIndicator:render()
if self.opacity == 0 then return end
local ass = assdraw.ass_new()
local is_static = self.type == 'static'
-- Background fadeout
if is_static then
if self.fadeout then
ass:rect(0, 0, display.width, display.height, {color = bg, opacity = self.opacity * 0.3})
end
-- Icon
local size = round(math.min(display.width, display.height) * (is_static and 0.20 or 0.15))
local size = round(math.min(display.width, display.height) * (self.fadeout and 0.20 or 0.15))
size = size + size * (1 - self.opacity)
if self.paused then

View File

@@ -27,6 +27,7 @@ function Speed:on_coordinates()
self.notch_spacing = self.width / (self.notches + 1)
self.font_size = round(self.height * 0.48 * options.font_scale)
end
function Speed:on_options() self:on_coordinates() end
function Speed:speed_step(speed, up)
if options.speed_step_is_factor then
@@ -86,12 +87,6 @@ function Speed:on_global_mouse_move()
end
function Speed:handle_cursor_up()
if self.proximity_raw == 0 then
-- Reset speed on short clicks
if self.dragging and math.abs(self.dragging.distance) < 6 and mp.get_time() - self.dragging.start_time < 0.15 then
mp.set_property_native('speed', 1)
end
end
self.dragging = nil
request_render()
end
@@ -110,22 +105,20 @@ function Speed:render()
if opacity <= 0 then return end
if self.proximity_raw == 0 then
cursor.on_primary_down = function()
self:handle_cursor_down()
cursor.on_primary_up = function() self:handle_cursor_up() end
end
cursor.on_wheel_down = function() self:handle_wheel_down() end
cursor.on_wheel_up = function() self:handle_wheel_up() end
end
if self.dragging then
cursor.on_primary_up = function() self:handle_cursor_up() end
end
cursor:zone('primary_down', self, function()
self:handle_cursor_down()
cursor:once('primary_up', function() self:handle_cursor_up() end)
end)
cursor:zone('secondary_click', self, function() mp.set_property_native('speed', 1) end)
cursor:zone('wheel_down', self, function() self:handle_wheel_down() end)
cursor:zone('wheel_up', self, function() self:handle_wheel_up() end)
local ass = assdraw.ass_new()
-- Background
ass:rect(self.ax, self.ay, self.bx, self.by, {color = bg, radius = 2, opacity = opacity * options.speed_opacity})
ass:rect(self.ax, self.ay, self.bx, self.by, {
color = bg, radius = state.radius, opacity = opacity * config.opacity.speed,
})
-- Coordinates
local ax, ay = self.ax, self.ay
@@ -163,7 +156,9 @@ function Speed:render()
end
ass:rect(notch_x - notch_thickness, notch_ay, notch_x + notch_thickness, notch_by, {
color = fg, border = 1, border_color = bg,
color = fg,
border = 1,
border_color = bg,
opacity = math.min(1.2 - (math.abs((notch_x - ax - half_width) / half_width)), 1) * opacity,
})
end
@@ -183,7 +178,11 @@ function Speed:render()
-- Speed value
local speed_text = (round(state.speed * 100) / 100) .. 'x'
ass:txt(half_x, ay + (notch_ay_big - ay) / 2, 5, speed_text, {
size = self.font_size, color = bgt, border = options.text_border, border_color = bg, opacity = opacity,
size = self.font_size,
color = bgt,
border = options.text_border * state.scale,
border_color = bg,
opacity = opacity,
})
return ass

View File

@@ -5,29 +5,29 @@ local Timeline = class(Element)
function Timeline:new() return Class.new(self) --[[@as Timeline]] end
function Timeline:init()
Element.init(self, 'timeline')
Element.init(self, 'timeline', {render_order = 5})
---@type false|{pause: boolean, distance: number, last: {x: number, y: number}}
self.pressed = false
self.obstructed = false
self.size_max = 0
self.size_min = 0
self.size_min_override = options.timeline_start_hidden and 0 or nil
self.size = 0
self.progress_size = 0
self.min_progress_size = 0 -- used for `flash-progress`
self.font_size = 0
self.top_border = options.timeline_border
self.top_border = 0
self.line_width = 0
self.progress_line_width = 0
self.is_hovered = false
self.has_thumbnail = false
-- Delayed seeking timer
self.seek_timer = mp.add_timeout(0.05, function() self:set_from_cursor() end)
self.seek_timer:kill()
self:decide_progress_size()
self:update_dimensions()
-- Release any dragging when file gets unloaded
mp.register_event('end-file', function() self.pressed = false end)
self:register_mp_event('end-file', function() self.pressed = false end)
end
function Timeline:get_visibility()
return Elements.controls and math.max(Elements.controls.proximity, Element.get_visibility(self))
or Element.get_visibility(self)
return math.max(Elements:maybe('controls', 'get_visibility') or 0, Element.get_visibility(self))
end
function Timeline:decide_enabled()
@@ -36,55 +36,78 @@ function Timeline:decide_enabled()
if self.enabled ~= previous then Elements:trigger('timeline_enabled', self.enabled) end
end
function Timeline:get_effective_size_min()
return self.size_min_override or self.size_min
end
function Timeline:get_effective_size()
if Elements.speed and Elements.speed.dragging then return self.size_max end
local size_min = self:get_effective_size_min()
return size_min + math.ceil((self.size_max - size_min) * self:get_visibility())
end
function Timeline:get_effective_line_width()
return state.fullormaxed and options.timeline_line_width_fullscreen or options.timeline_line_width
if Elements:v('speed', 'dragging') then return self.size end
local progress_size = math.max(self.min_progress_size, self.progress_size)
return progress_size + math.ceil((self.size - self.progress_size) * self:get_visibility())
end
function Timeline:get_is_hovered() return self.enabled and self.is_hovered end
function Timeline:update_dimensions()
if state.fullormaxed then
self.size_min = options.timeline_size_min_fullscreen
self.size_max = options.timeline_size_max_fullscreen
else
self.size_min = options.timeline_size_min
self.size_max = options.timeline_size_max
end
self.font_size = math.floor(math.min((self.size_max + 60) * 0.2, self.size_max * 0.96) * options.font_scale)
self.ax = Elements.window_border.size
self.ay = display.height - Elements.window_border.size - self.size_max - self.top_border
self.bx = display.width - Elements.window_border.size
self.by = display.height - Elements.window_border.size
self.size = round(options.timeline_size * state.scale)
self.top_border = round(options.timeline_border * state.scale)
self.line_width = round(options.timeline_line_width * state.scale)
self.progress_line_width = round(options.progress_line_width * state.scale)
self.font_size = math.floor(math.min((self.size + 60 * state.scale) * 0.2, self.size * 0.96) * options.font_scale)
local window_border_size = Elements:v('window_border', 'size', 0)
self.ax = window_border_size
self.ay = display.height - window_border_size - self.size - self.top_border
self.bx = display.width - window_border_size
self.by = display.height - window_border_size
self.width = self.bx - self.ax
self.chapter_size = math.max((self.by - self.ay) / 10, 3)
self.chapter_size_hover = self.chapter_size * 2
-- Disable if not enough space
local available_space = display.height - Elements.window_border.size * 2
if Elements.top_bar.enabled then available_space = available_space - Elements.top_bar.size end
self.obstructed = available_space < self.size_max + 10
local available_space = display.height - window_border_size * 2 - Elements:v('top_bar', 'size', 0)
self.obstructed = available_space < self.size + 10
self:decide_enabled()
end
function Timeline:decide_progress_size()
local show = options.progress == 'always'
or (options.progress == 'fullscreen' and state.fullormaxed)
or (options.progress == 'windowed' and not state.fullormaxed)
self.progress_size = show and options.progress_size or 0
end
function Timeline:toggle_progress()
local current = self.progress_size
self:tween_property('progress_size', current, current > 0 and 0 or options.progress_size)
request_render()
end
function Timeline:flash_progress()
if self.enabled and options.flash_duration > 0 then
if not self._flash_progress_timer then
self._flash_progress_timer = mp.add_timeout(options.flash_duration / 1000, function()
self:tween_property('min_progress_size', options.progress_size, 0)
end)
self._flash_progress_timer:kill()
end
self:tween_stop()
self.min_progress_size = options.progress_size
request_render()
self._flash_progress_timer.timeout = options.flash_duration / 1000
self._flash_progress_timer:kill()
self._flash_progress_timer:resume()
end
end
function Timeline:get_time_at_x(x)
local line_width = (options.timeline_style == 'line' and self:get_effective_line_width() - 1 or 0)
local line_width = (options.timeline_style == 'line' and self.line_width - 1 or 0)
local time_width = self.width - line_width - 1
local fax = (time_width) * state.time / state.duration
local fbx = fax + line_width
-- time starts 0.5 pixels in
x = x - self.ax - 0.5
if x > fbx then x = x - line_width
elseif x > fax then x = fax end
if x > fbx then
x = x - line_width
elseif x > fax then
x = fax
end
local progress = clamp(0, x / time_width, 1)
return state.duration * progress
end
@@ -105,15 +128,21 @@ function Timeline:handle_cursor_down()
self.pressed = {pause = state.pause, distance = 0, last = {x = cursor.x, y = cursor.y}}
mp.set_property_native('pause', true)
self:set_from_cursor()
cursor.on_primary_up = function() self:handle_cursor_up() end
end
function Timeline:on_prop_duration() self:decide_enabled() end
function Timeline:on_prop_time() self:decide_enabled() end
function Timeline:on_prop_border() self:update_dimensions() end
function Timeline:on_prop_fullormaxed() self:update_dimensions() end
function Timeline:on_prop_title_bar() self:update_dimensions() end
function Timeline:on_prop_fullormaxed()
self:decide_progress_size()
self:update_dimensions()
end
function Timeline:on_display() self:update_dimensions() end
function Timeline:on_options()
self:decide_progress_size()
self:update_dimensions()
end
function Timeline:handle_cursor_up()
self.seek_timer:kill()
if self.pressed then
mp.set_property_native('pause', self.pressed.pause)
self.pressed = false
@@ -127,20 +156,17 @@ function Timeline:on_global_mouse_move()
if self.pressed then
self.pressed.distance = self.pressed.distance + get_point_to_point_proximity(self.pressed.last, cursor)
self.pressed.last.x, self.pressed.last.y = cursor.x, cursor.y
if self.width / state.duration < 10 then
if state.is_video and math.abs(cursor:get_velocity().x) / self.width * state.duration > 30 then
self:set_from_cursor(true)
self.seek_timer:kill()
self.seek_timer:resume()
else self:set_from_cursor() end
else
self:set_from_cursor()
end
end
end
function Timeline:handle_wheel_up() mp.commandv('seek', options.timeline_step) end
function Timeline:handle_wheel_down() mp.commandv('seek', -options.timeline_step) end
function Timeline:render()
if self.size_max == 0 then return end
if self.size == 0 then return end
local size_min = self:get_effective_size_min()
local size = self:get_effective_size()
local visibility = self:get_visibility()
self.is_hovered = false
@@ -152,23 +178,30 @@ function Timeline:render()
if self.proximity_raw == 0 then
self.is_hovered = true
cursor.on_primary_down = function() self:handle_cursor_down() end
cursor.on_wheel_down = function() self:handle_wheel_down() end
cursor.on_wheel_up = function() self:handle_wheel_up() end
end
if self.pressed then
cursor.on_primary_up = function() self:handle_cursor_up() end
if visibility > 0 then
cursor:zone('primary_down', self, function()
self:handle_cursor_down()
cursor:once('primary_up', function() self:handle_cursor_up() end)
end)
if options.timeline_step ~= 0 then
cursor:zone('wheel_down', self, function() mp.commandv('seek', -options.timeline_step) end)
cursor:zone('wheel_up', self, function() mp.commandv('seek', options.timeline_step) end)
end
end
local ass = assdraw.ass_new()
local progress_size = math.max(self.min_progress_size, self.progress_size)
-- Text opacity rapidly drops to 0 just before it starts overflowing, or before it reaches timeline.size_min
local hide_text_below = math.max(self.font_size * 0.8, size_min * 2)
-- Text opacity rapidly drops to 0 just before it starts overflowing, or before it reaches progress_size
local hide_text_below = math.max(self.font_size * 0.8, progress_size * 2)
local hide_text_ramp = hide_text_below / 2
local text_opacity = clamp(0, size - hide_text_below, hide_text_ramp) / hide_text_ramp
local spacing = math.max(math.floor((self.size_max - self.font_size) / 2.5), 4)
local tooltip_gap = round(2 * state.scale)
local timestamp_gap = tooltip_gap
local spacing = math.max(math.floor((self.size - self.font_size) / 2.5), 4)
local progress = state.time / state.duration
local is_line = options.timeline_style == 'line'
@@ -180,12 +213,9 @@ function Timeline:render()
local line_width = 0
if is_line then
local minimized_fraction = 1 - math.min((size - size_min) / ((self.size_max - size_min) / 8), 1)
local line_width_max = self:get_effective_line_width()
local max_min_width_delta = size_min > 0
and line_width_max - line_width_max * options.timeline_line_width_minimized_scale
or 0
line_width = line_width_max - (max_min_width_delta * minimized_fraction)
local minimized_fraction = 1 - math.min((size - progress_size) / ((self.size - progress_size) / 8), 1)
local progress_delta = progress_size > 0 and self.progress_line_width - self.line_width or 0
line_width = self.line_width + (progress_delta * minimized_fraction)
fax = bax + (self.width - line_width) * progress
fbx = fax + line_width
line_width = line_width - 1
@@ -210,7 +240,7 @@ function Timeline:render()
ass:new_event()
ass:pos(0, 0)
ass:append('{\\rDefault\\an7\\blur0\\bord0\\1c&H' .. bg .. '}')
ass:opacity(options.timeline_opacity)
ass:opacity(config.opacity.timeline)
ass:draw_start()
ass:rect_cw(bax, bay, fax, bby) --left of progress
ass:rect_cw(fbx, bay, bbx, bby) --right of progress
@@ -218,18 +248,14 @@ function Timeline:render()
ass:draw_stop()
-- Progress
ass:rect(fax, fay, fbx, fby, {opacity = options.timeline_opacity})
ass:rect(fax, fay, fbx, fby, {opacity = config.opacity.position})
-- Uncached ranges
local buffered_playtime = nil
if state.uncached_ranges then
local opts = {size = 80, anchor_y = fby}
local texture_char = visibility > 0 and 'b' or 'a'
local offset = opts.size / (visibility > 0 and 24 or 28)
for _, range in ipairs(state.uncached_ranges) do
if not buffered_playtime and (range[1] > state.time or range[2] > state.time) then
buffered_playtime = (range[1] - state.time) / (state.speed or 1)
end
if options.timeline_cache then
local ax = range[1] < 0.5 and bax or math.floor(t2x(range[1]))
local bx = range[2] > state.duration - 0.5 and bbx or math.ceil(t2x(range[2]))
@@ -251,10 +277,8 @@ function Timeline:render()
-- Chapters
local hovered_chapter = nil
if (options.timeline_chapters_opacity > 0
and (#state.chapters > 0 or state.ab_loop_a or state.ab_loop_b)
) then
local diamond_radius = foreground_size < 3 and foreground_size or self.chapter_size
if (config.opacity.chapters > 0 and (#state.chapters > 0 or state.ab_loop_a or state.ab_loop_b)) then
local diamond_radius = math.min(math.max(1, foreground_size * 0.8), self.chapter_size)
local diamond_radius_hovered = diamond_radius * 2
local diamond_border = options.timeline_border and math.max(options.timeline_border, 1) or 1
@@ -264,7 +288,7 @@ function Timeline:render()
ass:new_event()
ass:append(string.format(
'{\\pos(0,0)\\rDefault\\an7\\blur0\\yshad0.01\\bord%f\\1c&H%s\\3c&H%s\\4c&H%s\\1a&H%X&\\3a&H00&\\4a&H00&}',
diamond_border, fg, bg, bg, opacity_to_alpha(options.timeline_opacity * options.timeline_chapters_opacity)
diamond_border, fg, bg, bg, opacity_to_alpha(config.opacity.chapters)
))
ass:draw_start()
ass:move_to(chapter_x - radius, chapter_y)
@@ -276,7 +300,7 @@ function Timeline:render()
if #state.chapters > 0 then
-- Find hovered chapter indicator
local closest_delta = INFINITY
local closest_delta = math.huge
if self.proximity_raw < diamond_radius_hovered then
for i, chapter in ipairs(state.chapters) do
@@ -285,19 +309,27 @@ function Timeline:render()
if cursor_chapter_delta <= diamond_radius_hovered and cursor_chapter_delta < closest_delta then
hovered_chapter, closest_delta = chapter, cursor_chapter_delta
self.is_hovered = true
cursor.on_primary_down = function()
mp.commandv('seek', hovered_chapter.time, 'absolute+exact')
end
end
end
end
for i, chapter in ipairs(state.chapters) do
if chapter ~= hovered_chapter then draw_chapter(chapter.time, diamond_radius) end
local circle = {point = {x = t2x(chapter.time), y = fay - 1}, r = diamond_radius_hovered}
if visibility > 0 then
cursor:zone('primary_click', circle, function()
mp.commandv('seek', chapter.time, 'absolute+exact')
end)
end
end
-- Render hovered chapter above others
if hovered_chapter then draw_chapter(hovered_chapter.time, diamond_radius_hovered) end
if hovered_chapter then
draw_chapter(hovered_chapter.time, diamond_radius_hovered)
timestamp_gap = tooltip_gap + round(diamond_radius_hovered)
else
timestamp_gap = tooltip_gap + round(diamond_radius)
end
end
-- A-B loop indicators
@@ -311,7 +343,7 @@ function Timeline:render()
ass:new_event()
ass:append(string.format(
'{\\pos(0,0)\\rDefault\\an7\\blur0\\yshad0.01\\bord%f\\1c&H%s\\3c&H%s\\4c&H%s\\1a&H%X&\\3a&H00&\\4a&H00&}',
diamond_border, fg, bg, bg, opacity_to_alpha(options.timeline_opacity * options.timeline_chapters_opacity)
diamond_border, fg, bg, bg, opacity_to_alpha(config.opacity.chapters)
))
ass:draw_start()
ass:move_to(x, fby - ab_radius)
@@ -327,47 +359,51 @@ function Timeline:render()
end
end
local function draw_timeline_text(x, y, align, text, opts)
local function draw_timeline_timestamp(x, y, align, timestamp, opts)
opts.color, opts.border_color = fgt, fg
opts.clip = '\\clip(' .. foreground_coordinates .. ')'
ass:txt(x, y, align, text, opts)
local func = options.time_precision > 0 and ass.timestamp or ass.txt
func(ass, x, y, align, timestamp, opts)
opts.color, opts.border_color = bgt, bg
opts.clip = '\\iclip(' .. foreground_coordinates .. ')'
ass:txt(x, y, align, text, opts)
func(ass, x, y, align, timestamp, opts)
end
-- Time values
if text_opacity > 0 then
local time_opts = {size = self.font_size, opacity = text_opacity, border = 2}
local time_opts = {size = self.font_size, opacity = text_opacity, border = 2 * state.scale}
-- Upcoming cache time
if buffered_playtime and options.buffered_time_threshold > 0
and buffered_playtime < options.buffered_time_threshold then
local x, align = fbx + 5, 4
local cache_opts = {size = self.font_size * 0.8, opacity = text_opacity * 0.6, border = 1}
local human = round(math.max(buffered_playtime, 0)) .. 's'
local cache_duration = state.cache_duration and state.cache_duration / state.speed or nil
if cache_duration and options.buffered_time_threshold > 0
and cache_duration < options.buffered_time_threshold then
local margin = 5 * state.scale
local x, align = fbx + margin, 4
local cache_opts = {
size = self.font_size * 0.8, opacity = text_opacity * 0.6, border = options.text_border * state.scale,
}
local human = round(cache_duration) .. 's'
local width = text_width(human, cache_opts)
local time_width = timestamp_width(state.time_human, time_opts)
local time_width_end = timestamp_width(state.destination_time_human, time_opts)
local min_x, max_x = bax + spacing + 5 + time_width, bbx - spacing - 5 - time_width_end
local min_x, max_x = bax + spacing + margin + time_width, bbx - spacing - margin - time_width_end
if x < min_x then x = min_x elseif x + width > max_x then x, align = max_x, 6 end
draw_timeline_text(x, fcy, align, human, cache_opts)
draw_timeline_timestamp(x, fcy, align, human, cache_opts)
end
-- Elapsed time
if state.time_human then
draw_timeline_text(bax + spacing, fcy, 4, state.time_human, time_opts)
draw_timeline_timestamp(bax + spacing, fcy, 4, state.time_human, time_opts)
end
-- End time
if state.destination_time_human then
draw_timeline_text(bbx - spacing, fcy, 6, state.destination_time_human, time_opts)
draw_timeline_timestamp(bbx - spacing, fcy, 6, state.destination_time_human, time_opts)
end
end
-- Hovered time and chapter
local rendered_thumbnail = false
if (self.proximity_raw == 0 or self.pressed or hovered_chapter) and
not (Elements.speed and Elements.speed.dragging) then
if (self.proximity_raw == 0 or self.pressed or hovered_chapter) and not Elements:v('speed', 'dragging') then
local cursor_x = hovered_chapter and t2x(hovered_chapter.time) or cursor.x
local hovered_seconds = hovered_chapter and hovered_chapter.time or self:get_time_at_x(cursor.x)
@@ -375,16 +411,16 @@ function Timeline:render()
-- 0.5 to switch when the pixel is half filled in
local color = ((fax - 0.5) < cursor_x and cursor_x < (fbx + 0.5)) and bg or fg
local ax, ay, bx, by = cursor_x - 0.5, fay, cursor_x + 0.5, fby
ass:rect(ax, ay, bx, by, {color = color, opacity = 0.2})
local tooltip_anchor = {ax = ax, ay = ay, bx = bx, by = by}
ass:rect(ax, ay, bx, by, {color = color, opacity = 0.33})
local tooltip_anchor = {ax = ax, ay = ay - self.top_border, bx = bx, by = by}
-- Timestamp
local offset = #state.chapters > 0 and 10 or 4
local opts = {size = self.font_size, offset = offset}
local opts = {
size = self.font_size, offset = timestamp_gap, margin = tooltip_gap, timestamp = options.time_precision > 0,
}
local hovered_time_human = format_time(hovered_seconds, state.duration)
opts.width_overwrite = timestamp_width(hovered_time_human, opts)
ass:tooltip(tooltip_anchor, hovered_time_human, opts)
tooltip_anchor.ay = tooltip_anchor.ay - self.font_size - offset
tooltip_anchor = ass:tooltip(tooltip_anchor, hovered_time_human, opts)
-- Thumbnail
if not thumbnail.disabled
@@ -392,30 +428,42 @@ function Timeline:render()
and thumbnail.width ~= 0
and thumbnail.height ~= 0
then
local scale_x, scale_y = display.scale_x, display.scale_y
local border, margin_x, margin_y = math.ceil(2 * scale_x), round(10 * scale_x), round(5 * scale_y)
local thumb_x_margin, thumb_y_margin = border + margin_x + bax, border + margin_y
local border = math.ceil(math.max(2, state.radius / 2) * state.scale)
local thumb_x_margin, thumb_y_margin = border + tooltip_gap + bax, border + tooltip_gap
local thumb_width, thumb_height = thumbnail.width, thumbnail.height
local thumb_x = round(clamp(
thumb_x_margin, cursor_x * scale_x - thumb_width / 2,
display.width * scale_x - thumb_width - thumb_x_margin
thumb_x_margin,
cursor_x - thumb_width / 2,
display.width - thumb_width - thumb_x_margin
))
local thumb_y = round(tooltip_anchor.ay * scale_y - thumb_y_margin - thumb_height)
local ax, ay = (thumb_x - border) / scale_x, (thumb_y - border) / scale_y
local bx, by = (thumb_x + thumb_width + border) / scale_x, (thumb_y + thumb_height + border) / scale_y
ass:rect(ax, ay, bx, by, {color = bg, border = 1, border_color = fg, border_opacity = 0.08, radius = 2})
local thumb_y = round(tooltip_anchor.ay - thumb_y_margin - thumb_height)
local ax, ay = (thumb_x - border), (thumb_y - border)
local bx, by = (thumb_x + thumb_width + border), (thumb_y + thumb_height + border)
ass:rect(ax, ay, bx, by, {
color = bg,
border = 1,
opacity = {main = config.opacity.thumbnail, border = 0.08 * config.opacity.thumbnail},
border_color = fg,
radius = state.radius,
})
mp.commandv('script-message-to', 'thumbfast', 'thumb', hovered_seconds, thumb_x, thumb_y)
self.has_thumbnail, rendered_thumbnail = true, true
tooltip_anchor.ax, tooltip_anchor.bx, tooltip_anchor.ay = ax, bx, ay
tooltip_anchor.ay = ay
end
-- Chapter title
if #state.chapters > 0 then
local _, chapter = itable_find(state.chapters, function(c) return hovered_seconds >= c.time end, #state.chapters, 1)
if config.opacity.chapters > 0 and #state.chapters > 0 then
local _, chapter = itable_find(state.chapters, function(c) return hovered_seconds >= c.time end,
#state.chapters, 1)
if chapter and not chapter.is_end_only then
ass:tooltip(tooltip_anchor, chapter.title_wrapped, {
size = self.font_size, offset = 10, responsive = false, bold = true,
size = self.font_size,
offset = tooltip_gap,
responsive = false,
bold = true,
width_overwrite = chapter.title_wrapped_width * self.font_size,
lines = chapter.title_lines,
margin = tooltip_gap,
})
end
end

View File

@@ -0,0 +1,334 @@
local Element = require('elements/Element')
---@alias TopBarButtonProps {icon: string; hover_fg?: string; hover_bg?: string; command: (fun():string)}
---@class TopBar : Element
local TopBar = class(Element)
function TopBar:new() return Class.new(self) --[[@as TopBar]] end
function TopBar:init()
Element.init(self, 'top_bar', {render_order = 4})
self.size = 0
self.icon_size, self.font_size, self.title_by = 1, 1, 1
self.show_alt_title = false
self.main_title, self.alt_title = nil, nil
local function maximized_command()
if state.platform == 'windows' then
mp.command(state.border
and (state.fullscreen and 'set fullscreen no;cycle window-maximized' or 'cycle window-maximized')
or 'set window-maximized no;cycle fullscreen')
else
mp.command(state.fullormaxed and 'set fullscreen no;set window-maximized no' or 'set window-maximized yes')
end
end
local close = {icon = 'close', hover_bg = '2311e8', hover_fg = 'ffffff', command = function() mp.command('quit') end}
local max = {icon = 'crop_square', command = maximized_command}
local min = {icon = 'minimize', command = function() mp.command('cycle window-minimized') end}
self.buttons = options.top_bar_controls == 'left' and {close, max, min} or {min, max, close}
self:decide_titles()
self:decide_enabled()
self:update_dimensions()
end
function TopBar:decide_enabled()
if options.top_bar == 'no-border' then
self.enabled = not state.border or state.title_bar == false or state.fullscreen
else
self.enabled = options.top_bar == 'always'
end
self.enabled = self.enabled and (options.top_bar_controls or options.top_bar_title ~= 'no' or state.has_playlist)
end
function TopBar:decide_titles()
self.alt_title = state.alt_title ~= '' and state.alt_title or nil
self.main_title = state.title ~= '' and state.title or nil
if (self.main_title == 'No file') then
self.main_title = t('No file')
end
-- Fall back to alt title if main is empty
if not self.main_title then
self.main_title, self.alt_title = self.alt_title, nil
end
-- Deduplicate the main and alt titles by checking if one completely
-- contains the other, and using only the longer one.
if self.main_title and self.alt_title and not self.show_alt_title then
local longer_title, shorter_title
if #self.main_title < #self.alt_title then
longer_title, shorter_title = self.alt_title, self.main_title
else
longer_title, shorter_title = self.main_title, self.alt_title
end
local escaped_shorter_title = string.gsub(shorter_title --[[@as string]], '[%(%)%.%+%-%*%?%[%]%^%$%%]', '%%%1')
if string.match(longer_title --[[@as string]], escaped_shorter_title) then
self.main_title, self.alt_title = longer_title, nil
end
end
end
function TopBar:update_dimensions()
self.size = round(options.top_bar_size * state.scale)
self.icon_size = round(self.size * 0.5)
self.font_size = math.floor((self.size - (math.ceil(self.size * 0.25) * 2)) * options.font_scale)
local window_border_size = Elements:v('window_border', 'size', 0)
self.ax = window_border_size
self.ay = window_border_size
self.bx = display.width - window_border_size
self.by = self.size + window_border_size
end
function TopBar:toggle_title()
if options.top_bar_alt_title_place ~= 'toggle' then return end
self.show_alt_title = not self.show_alt_title
request_render()
end
function TopBar:on_prop_title() self:decide_titles() end
function TopBar:on_prop_alt_title() self:decide_titles() end
function TopBar:on_prop_border()
self:decide_enabled()
self:update_dimensions()
end
function TopBar:on_prop_title_bar()
self:decide_enabled()
self:update_dimensions()
end
function TopBar:on_prop_fullscreen()
self:decide_enabled()
self:update_dimensions()
end
function TopBar:on_prop_maximized()
self:decide_enabled()
self:update_dimensions()
end
function TopBar:on_prop_has_playlist()
self:decide_enabled()
self:update_dimensions()
end
function TopBar:on_display() self:update_dimensions() end
function TopBar:on_options()
self:decide_enabled()
self:update_dimensions()
end
function TopBar:render()
local visibility = self:get_visibility()
if visibility <= 0 then return end
local ass = assdraw.ass_new()
local ax, bx = self.ax, self.bx
local margin = math.floor((self.size - self.font_size) / 4)
-- Window controls
if options.top_bar_controls then
local is_left, button_ax = options.top_bar_controls == 'left', 0
if is_left then
button_ax = ax
ax = self.size * #self.buttons
else
button_ax = bx - self.size * #self.buttons
bx = button_ax
end
for _, button in ipairs(self.buttons) do
local rect = {ax = button_ax, ay = self.ay, bx = button_ax + self.size, by = self.by}
local is_hover = get_point_to_rectangle_proximity(cursor, rect) == 0
local opacity = is_hover and 1 or config.opacity.controls
local button_fg = is_hover and (button.hover_fg or bg) or fg
local button_bg = is_hover and (button.hover_bg or fg) or bg
cursor:zone('primary_click', rect, button.command)
local bg_size = self.size - margin
local bg_ax, bg_ay = rect.ax + (is_left and margin or 0), rect.ay + margin
local bg_bx, bg_by = bg_ax + bg_size, bg_ay + bg_size
ass:rect(bg_ax, bg_ay, bg_bx, bg_by, {
color = button_bg, opacity = visibility * opacity, radius = state.radius,
})
ass:icon(bg_ax + bg_size / 2, bg_ay + bg_size / 2, bg_size * 0.5, button.icon, {
color = button_fg,
border_color = button_bg,
opacity = visibility,
border = options.text_border * state.scale,
})
button_ax = button_ax + self.size
end
end
-- Window title
if state.title or state.has_playlist then
local padding = self.font_size / 2
local spacing = 1
local left_aligned = options.top_bar_controls == 'left'
local title_ax, title_bx, title_ay = ax + margin, bx - margin, self.ay + margin
-- Playlist position
if state.has_playlist then
local text = state.playlist_pos .. '' .. state.playlist_count
local formatted_text = '{\\b1}' .. state.playlist_pos .. '{\\b0\\fs' .. self.font_size * 0.9 .. '}/'
.. state.playlist_count
local opts = {size = self.font_size, wrap = 2, color = fgt, opacity = visibility}
local rect_width = round(text_width(text, opts) + padding * 2)
local ax = left_aligned and title_bx - rect_width or title_ax
local rect = {
ax = ax,
ay = title_ay,
bx = ax + rect_width,
by = self.by - margin,
}
local opacity = get_point_to_rectangle_proximity(cursor, rect) == 0
and 1 or config.opacity.playlist_position
if opacity > 0 then
ass:rect(rect.ax, rect.ay, rect.bx, rect.by, {
color = fg, opacity = visibility * opacity, radius = state.radius,
})
end
ass:txt(rect.ax + (rect.bx - rect.ax) / 2, rect.ay + (rect.by - rect.ay) / 2, 5, formatted_text, opts)
if left_aligned then title_bx = rect.ax - margin else title_ax = rect.bx + margin end
-- Click action
cursor:zone('primary_click', rect, function() mp.command('script-binding uosc/playlist') end)
end
-- Skip rendering titles if there's not enough horizontal space
if title_bx - title_ax > self.font_size * 3 and options.top_bar_title ~= 'no' then
-- Main title
local main_title = self.show_alt_title and self.alt_title or self.main_title
if main_title then
local opts = {
size = self.font_size,
wrap = 2,
color = bgt,
opacity = visibility,
border = options.text_border * state.scale,
border_color = bg,
clip = string.format('\\clip(%d, %d, %d, %d)', self.ax, self.ay, title_bx, self.by),
}
local rect_ideal_width = round(text_width(main_title, opts) + padding * 2)
local rect_width = math.min(rect_ideal_width, title_bx - title_ax)
local ax = left_aligned and title_bx - rect_width or title_ax
local by = self.by - margin
local title_rect = {ax = ax, ay = title_ay, bx = ax + rect_width, by = by}
if options.top_bar_alt_title_place == 'toggle' then
cursor:zone('primary_click', title_rect, function() self:toggle_title() end)
end
ass:rect(title_rect.ax, title_rect.ay, title_rect.bx, title_rect.by, {
color = bg, opacity = visibility * config.opacity.title, radius = state.radius,
})
local align = left_aligned and rect_ideal_width == rect_width and 6 or 4
local x = align == 6 and title_rect.bx - padding or ax + padding
ass:txt(x, self.ay + (self.size / 2), align, main_title, opts)
title_ay = by + spacing
end
-- Alt title
if self.alt_title and options.top_bar_alt_title_place == 'below' then
local font_size = self.font_size * 0.9
local height = font_size * 1.3
local by = title_ay + height
local opts = {
size = font_size,
wrap = 2,
color = bgt,
border = options.text_border * state.scale,
border_color = bg,
opacity = visibility,
}
local rect_ideal_width = round(text_width(self.alt_title, opts) + padding * 2)
local rect_width = math.min(rect_ideal_width, title_bx - title_ax)
local ax = left_aligned and title_bx - rect_width or title_ax
local bx = ax + rect_width
opts.clip = string.format('\\clip(%d, %d, %d, %d)', title_ax, title_ay, bx, by)
ass:rect(ax, title_ay, bx, by, {
color = bg, opacity = visibility * config.opacity.title, radius = state.radius,
})
local align = left_aligned and rect_ideal_width == rect_width and 6 or 4
local x = align == 6 and bx - padding or ax + padding
ass:txt(x, title_ay + height / 2, align, self.alt_title, opts)
title_ay = by + spacing
end
-- Current chapter
if state.current_chapter then
local padding_half = round(padding / 2)
local font_size = self.font_size * 0.8
local height = font_size * 1.3
local prefix, postfix = left_aligned and '' or '', left_aligned and '' or ''
local text = prefix .. state.current_chapter.index .. ': ' .. state.current_chapter.title .. postfix
local next_chapter = state.chapters[state.current_chapter.index + 1]
local chapter_end = next_chapter and next_chapter.time or state.duration or 0
local remaining_time = ((state.time or 0) - chapter_end) /
(options.destination_time == 'time-remaining' and 1 or state.speed)
local remaining_human = format_time(remaining_time, math.abs(remaining_time))
local opts = {
size = font_size,
italic = true,
wrap = 2,
color = bgt,
border = options.text_border * state.scale,
border_color = bg,
opacity = visibility * 0.8,
}
local remaining_width = timestamp_width(remaining_human, opts)
local remaining_box_width = remaining_width + padding_half * 2
-- Title
local max_bx = title_bx - remaining_box_width - spacing
local rect_ideal_width = round(text_width(text, opts) + padding * 2)
local rect_width = math.min(rect_ideal_width, max_bx - title_ax)
local ax = left_aligned and title_bx - rect_width or title_ax
local rect = {
ax = ax,
ay = title_ay,
bx = ax + rect_width,
by = title_ay + height,
}
opts.clip = string.format('\\clip(%d, %d, %d, %d)', title_ax, title_ay, rect.bx, rect.by)
ass:rect(rect.ax, rect.ay, rect.bx, rect.by, {
color = bg, opacity = visibility * config.opacity.title, radius = state.radius,
})
local align = left_aligned and rect_ideal_width == rect_width and 6 or 4
local x = align == 6 and rect.bx - padding or rect.ax + padding
ass:txt(x, rect.ay + height / 2, align, text, opts)
-- Click action
cursor:zone('primary_click', rect, function() mp.command('script-binding uosc/chapters') end)
-- Time
rect.ax = left_aligned and rect.ax - spacing - remaining_box_width or rect.bx + spacing
rect.bx = rect.ax + remaining_box_width
opts.clip = nil
ass:rect(rect.ax, rect.ay, rect.bx, rect.by, {
color = bg, opacity = visibility * config.opacity.title, radius = state.radius,
})
ass:txt(rect.ax + padding_half, rect.ay + height / 2, 4, remaining_human, opts)
title_ay = rect.by + spacing
end
end
self.title_by = title_ay - 1
else
self.title_by = self.ay
end
return ass
end
return TopBar

View File

@@ -0,0 +1,170 @@
local Element = require('elements/Element')
local dots = {'.', '..', '...'}
local function cleanup_output(output)
return tostring(output):gsub('%c*\n%c*', '\n'):match('^[%s%c]*(.-)[%s%c]*$')
end
---@class Updater : Element
local Updater = class(Element)
function Updater:new() return Class.new(self) --[[@as Updater]] end
function Updater:init()
Element.init(self, 'updater', {render_order = 1000})
self.output = nil
self.message = t('Updating uosc')
self.state = 'pending' -- Matches icon name
local config_dir = mp.command_native({'expand-path', '~~/'})
Elements:maybe('curtain', 'register', self.id)
local function handle_result(success, result, error)
if success and result and result.status == 0 then
self.state = 'done'
self.message = t('uosc has been installed. Restart mpv for it to take effect.')
else
self.state = 'error'
self.message = t('An error has occurred.') .. ' ' .. t('See above for clues.')
end
local output = (result.stdout or '') .. '\n' .. (error or result.stderr or '')
if state.platform == 'darwin' then
output =
'Self-updater is known not to work on MacOS.\nIf you know about a solution, please make an issue and share it with us!.\n' ..
output
end
self.output = ass_escape(cleanup_output(output))
request_render()
end
local function update(args)
local env = utils.get_env_list()
env[#env + 1] = 'MPV_CONFIG_DIR=' .. config_dir
mp.command_native_async({
name = 'subprocess',
capture_stderr = true,
capture_stdout = true,
playback_only = false,
args = args,
env = env,
}, handle_result)
end
if state.platform == 'windows' then
local url = 'https://raw.githubusercontent.com/tomasklaen/uosc/HEAD/installers/windows.ps1'
update({'powershell', '-NoProfile', '-Command', 'irm ' .. url .. ' | iex'})
else
-- Detect missing dependencies. We can't just let the process run and
-- report an error, as on snap packages there's no error. Everything
-- either exits with 0, or no helpful output/error message.
local missing = {}
for _, name in ipairs({'curl', 'unzip'}) do
local result = mp.command_native({
name = 'subprocess',
capture_stdout = true,
playback_only = false,
args = {'which', name},
})
local path = cleanup_output(result and result.stdout or '')
if path == '' then
missing[#missing + 1] = name
end
end
if #missing > 0 then
local stderr = 'Missing dependencies: ' .. table.concat(missing, ', ')
if config_dir:match('/snap/') then
stderr = stderr ..
'\nThis is a known error for mpv snap packages.\nYou can still update uosc by entering the Linux install command from uosc\'s readme into your terminal, it just can\'t be done this way.\nIf you know about a solution, please make an issue and share it with us!'
end
handle_result(false, {stderr = stderr})
else
local url = 'https://raw.githubusercontent.com/tomasklaen/uosc/HEAD/installers/unix.sh'
update({'/bin/bash', '-c', 'source <(curl -fsSL ' .. url .. ')'})
end
end
end
function Updater:destroy()
Elements:maybe('curtain', 'unregister', self.id)
Element.destroy(self)
end
function Updater:render()
local ass = assdraw.ass_new()
local text_size = math.min(20 * state.scale, display.height / 20)
local icon_size = text_size * 2
local center_x = round(display.width / 2)
local color = fg
if self.state == 'done' then
color = config.color.success
elseif self.state == 'error' then
color = config.color.error
end
-- Divider
local divider_width = round(math.min(500 * state.scale, display.width * 0.8))
local divider_half, divider_border_half, divider_y = divider_width / 2, round(1 * state.scale), display.height * 0.65
local divider_ay, divider_by = round(divider_y - divider_border_half), round(divider_y + divider_border_half)
ass:rect(center_x - divider_half, divider_ay, center_x - icon_size, divider_by, {
color = color, border = options.text_border * state.scale, border_color = bg, opacity = 0.5,
})
ass:rect(center_x + icon_size, divider_ay, center_x + divider_half, divider_by, {
color = color, border = options.text_border * state.scale, border_color = bg, opacity = 0.5,
})
if self.state == 'pending' then
ass:spinner(center_x, divider_y, icon_size, {
color = fg, border = options.text_border * state.scale, border_color = bg,
})
else
ass:icon(center_x, divider_y, icon_size * 0.8, self.state, {
color = color, border = options.text_border * state.scale, border_color = bg,
})
end
-- Output
local output = self.output or dots[math.ceil((mp.get_time() % 1) * #dots)]
ass:txt(center_x, divider_y - icon_size, 2, output, {
size = text_size, color = fg, border = options.text_border * state.scale, border_color = bg,
})
-- Message
ass:txt(center_x, divider_y + icon_size, 5, self.message, {
size = text_size, bold = true, color = color, border = options.text_border * state.scale, border_color = bg,
})
-- Button
if self.state ~= 'pending' then
-- Background
local button_y = divider_y + icon_size * 1.75
local button_rect = {
ax = round(center_x - icon_size / 2),
ay = round(button_y),
bx = round(center_x + icon_size / 2),
by = round(button_y + icon_size),
}
local is_hovered = get_point_to_rectangle_proximity(cursor, button_rect) == 0
ass:rect(button_rect.ax, button_rect.ay, button_rect.bx, button_rect.by, {
color = fg,
radius = state.radius,
opacity = is_hovered and 1 or 0.5,
})
-- Icon
local x = round(button_rect.ax + (button_rect.bx - button_rect.ax) / 2)
local y = round(button_rect.ay + (button_rect.by - button_rect.ay) / 2)
ass:icon(x, y, icon_size * 0.8, 'close', {color = bg})
cursor:zone('primary_click', button_rect, function() self:destroy() end)
end
return ass
end
return Updater

View File

@@ -1,27 +1,5 @@
local Element = require('elements/Element')
--[[ MuteButton ]]
---@class MuteButton : Element
local MuteButton = class(Element)
---@param props? ElementProps
function MuteButton:new(props) return Class.new(self, 'volume_mute', props) --[[@as MuteButton]] end
function MuteButton:get_visibility() return Elements.volume:get_visibility(self) end
function MuteButton:render()
local visibility = self:get_visibility()
if visibility <= 0 then return end
if self.proximity_raw == 0 then
cursor.on_primary_down = function() mp.commandv('cycle', 'mute') end
end
local ass = assdraw.ass_new()
local icon_name = state.mute and 'volume_off' or 'volume_up'
local width = self.bx - self.ax
ass:icon(self.ax + (width / 2), self.by, width * 0.7, icon_name,
{border = options.text_border, opacity = options.volume_opacity * visibility, align = 2}
)
return ass
end
--[[ VolumeSlider ]]
---@class VolumeSlider : Element
@@ -35,7 +13,12 @@ function VolumeSlider:init(props)
self.nudge_size = 0
self.draw_nudge = false
self.spacing = 0
self.radius = 1
self.border_size = 0
self:update_dimensions()
end
function VolumeSlider:update_dimensions()
self.border_size = math.max(0, round(options.volume_border * state.scale))
end
function VolumeSlider:get_visibility() return Elements.volume:get_visibility(self) end
@@ -47,10 +30,12 @@ function VolumeSlider:set_volume(volume)
end
function VolumeSlider:set_from_cursor()
local volume_fraction = (self.by - cursor.y - options.volume_border) / (self.by - self.ay - options.volume_border)
local volume_fraction = (self.by - cursor.y - self.border_size) / (self.by - self.ay - self.border_size)
self:set_volume(volume_fraction * state.volume_max)
end
function VolumeSlider:on_display() self:update_dimensions() end
function VolumeSlider:on_options() self:update_dimensions() end
function VolumeSlider:on_coordinates()
if type(state.volume_max) ~= 'number' or state.volume_max <= 0 then return end
local width = self.bx - self.ax
@@ -58,7 +43,6 @@ function VolumeSlider:on_coordinates()
self.nudge_size = round(width * 0.18)
self.draw_nudge = self.ay < self.nudge_y
self.spacing = round(width * 0.2)
self.radius = math.max(2, (self.bx - self.ax) / 10)
end
function VolumeSlider:on_global_mouse_move()
if self.pressed then self:set_from_cursor() end
@@ -73,31 +57,26 @@ function VolumeSlider:render()
if width <= 0 or height <= 0 or visibility <= 0 then return end
if self.proximity_raw == 0 then
cursor.on_primary_down = function()
self.pressed = true
self:set_from_cursor()
cursor.on_primary_up = function() self.pressed = false end
end
cursor.on_wheel_down = function() self:handle_wheel_down() end
cursor.on_wheel_up = function() self:handle_wheel_up() end
end
if self.pressed then cursor.on_primary_up = function()
self.pressed = false end
end
cursor:zone('primary_down', self, function()
self.pressed = true
self:set_from_cursor()
cursor:once('primary_up', function() self.pressed = false end)
end)
cursor:zone('wheel_down', self, function() self:handle_wheel_down() end)
cursor:zone('wheel_up', self, function() self:handle_wheel_up() end)
local ass = assdraw.ass_new()
local nudge_y, nudge_size = self.draw_nudge and self.nudge_y or -INFINITY, self.nudge_size
local volume_y = self.ay + options.volume_border +
((height - (options.volume_border * 2)) * (1 - math.min(state.volume / state.volume_max, 1)))
local nudge_y, nudge_size = self.draw_nudge and self.nudge_y or -math.huge, self.nudge_size
local volume_y = self.ay + self.border_size +
((height - (self.border_size * 2)) * (1 - math.min(state.volume / state.volume_max, 1)))
-- Draws a rectangle with nudge at requested position
---@param p number Padding from slider edges.
---@param r number Border radius.
---@param cy? number A y coordinate where to clip the path from the bottom.
function create_nudged_path(p, cy)
function create_nudged_path(p, r, cy)
cy = cy or ay + p
local ax, bx, by = ax + p, bx - p, by - p
local r = math.max(1, self.radius - p)
local d, rh = r * 2, r / 2
local nudge_size = ((QUARTER_PI_SIN * (nudge_size - p)) + p) / QUARTER_PI_SIN
local path = assdraw.ass_new()
@@ -155,14 +134,14 @@ function VolumeSlider:render()
end
-- BG & FG paths
local bg_path = create_nudged_path(0)
local fg_path = create_nudged_path(options.volume_border, volume_y)
local bg_path = create_nudged_path(0, state.radius + self.border_size)
local fg_path = create_nudged_path(self.border_size, state.radius, volume_y)
-- Background
ass:new_event()
ass:append('{\\rDefault\\an7\\blur0\\bord0\\1c&H' .. bg ..
'\\iclip(' .. fg_path.scale .. ', ' .. fg_path.text .. ')}')
ass:opacity(options.volume_opacity, visibility)
ass:opacity(config.opacity.slider, visibility)
ass:pos(0, 0)
ass:draw_start()
ass:append(bg_path.text)
@@ -171,7 +150,7 @@ function VolumeSlider:render()
-- Foreground
ass:new_event()
ass:append('{\\rDefault\\an7\\blur0\\bord0\\1c&H' .. fg .. '}')
ass:opacity(options.volume_opacity, visibility)
ass:opacity(config.opacity.slider_gauge, visibility)
ass:pos(0, 0)
ass:draw_start()
ass:append(fg_path.text)
@@ -182,22 +161,29 @@ function VolumeSlider:render()
local font_size = round(((width * 0.6) - (#volume_string * (width / 20))) * options.font_scale)
if volume_y < self.by - self.spacing then
ass:txt(self.ax + (width / 2), self.by - self.spacing, 2, volume_string, {
size = font_size, color = fgt, opacity = visibility,
size = font_size,
color = fgt,
opacity = visibility,
clip = '\\clip(' .. fg_path.scale .. ', ' .. fg_path.text .. ')',
})
end
if volume_y > self.by - self.spacing - font_size then
ass:txt(self.ax + (width / 2), self.by - self.spacing, 2, volume_string, {
size = font_size, color = bgt, opacity = visibility,
size = font_size,
color = bgt,
opacity = visibility,
clip = '\\iclip(' .. fg_path.scale .. ', ' .. fg_path.text .. ')',
})
end
-- Disabled stripes for no audio
if not state.has_audio then
local fg_100_path = create_nudged_path(options.volume_border)
local fg_100_path = create_nudged_path(self.border_size, state.radius)
local texture_opts = {
size = 200, color = 'ffffff', opacity = visibility * 0.1, anchor_x = ax,
size = 200,
color = 'ffffff',
opacity = visibility * 0.1,
anchor_x = ax,
clip = '\\clip(' .. fg_100_path.scale .. ',' .. fg_100_path.text .. ')',
}
ass:texture(ax, ay, bx, by, 'a', texture_opts)
@@ -216,37 +202,81 @@ local Volume = class(Element)
function Volume:new() return Class.new(self) --[[@as Volume]] end
function Volume:init()
Element.init(self, 'volume')
self.mute = MuteButton:new({anchor_id = 'volume'})
self.slider = VolumeSlider:new({anchor_id = 'volume'})
Element.init(self, 'volume', {render_order = 7})
self.size = 0
self.mute_ay = 0
self.slider = VolumeSlider:new({anchor_id = 'volume', render_order = self.render_order})
self:update_dimensions()
end
function Volume:destroy()
self.slider:destroy()
Element.destroy(self)
end
function Volume:get_visibility()
return self.slider.pressed and 1 or Elements.timeline:get_is_hovered() and -1 or Element.get_visibility(self)
return self.slider.pressed and 1 or Elements:maybe('timeline', 'get_is_hovered') and -1
or Element.get_visibility(self)
end
function Volume:update_dimensions()
local width = state.fullormaxed and options.volume_size_fullscreen or options.volume_size
local controls, timeline, top_bar = Elements.controls, Elements.timeline, Elements.top_bar
local min_y = top_bar.enabled and top_bar.by or 0
local max_y = (controls and controls.enabled and controls.ay) or (timeline.enabled and timeline.ay)
or display.height - top_bar.size
self.size = round(options.volume_size * state.scale)
local min_y = Elements:v('top_bar', 'by') or Elements:v('window_border', 'size', 0)
local max_y = Elements:v('controls', 'ay') or Elements:v('timeline', 'ay')
or display.height - Elements:v('window_border', 'size', 0)
local available_height = max_y - min_y
local max_height = available_height * 0.8
local height = round(math.min(width * 8, max_height))
self.enabled = height > width * 2 -- don't render if too small
local margin = (width / 2) + Elements.window_border.size
self.ax = round(options.volume == 'left' and margin or display.width - margin - width)
local height = round(math.min(self.size * 8, max_height))
self.enabled = height > self.size * 2 -- don't render if too small
local margin = (self.size / 2) + Elements:v('window_border', 'size', 0)
self.ax = round(options.volume == 'left' and margin or display.width - margin - self.size)
self.ay = min_y + round((available_height - height) / 2)
self.bx = round(self.ax + width)
self.bx = round(self.ax + self.size)
self.by = round(self.ay + height)
self.mute.enabled, self.slider.enabled = self.enabled, self.enabled
self.mute:set_coordinates(self.ax, self.by - round(width * 0.8), self.bx, self.by)
self.slider:set_coordinates(self.ax, self.ay, self.bx, self.mute.ay)
self.mute_ay = self.by - self.size
self.slider.enabled = self.enabled
self.slider:set_coordinates(self.ax, self.ay, self.bx, self.mute_ay)
end
function Volume:on_display() self:update_dimensions() end
function Volume:on_prop_border() self:update_dimensions() end
function Volume:on_prop_title_bar() self:update_dimensions() end
function Volume:on_controls_reflow() self:update_dimensions() end
function Volume:on_options() self:update_dimensions() end
function Volume:render()
local visibility = self:get_visibility()
if visibility <= 0 then return end
-- Reset volume on secondary click
cursor:zone('secondary_click', self, function()
mp.set_property_native('mute', false)
mp.set_property_native('volume', 100)
end)
-- Mute button
local mute_rect = {ax = self.ax, ay = self.mute_ay, bx = self.bx, by = self.by}
cursor:zone('primary_click', mute_rect, function() mp.commandv('cycle', 'mute') end)
local ass = assdraw.ass_new()
local width_half = (mute_rect.bx - mute_rect.ax) / 2
local height_half = (mute_rect.by - mute_rect.ay) / 2
local icon_size = math.min(width_half, height_half) * 1.5
local icon_name, horizontal_shift = 'volume_up', 0
if state.mute then
icon_name = 'volume_off'
elseif state.volume <= 0 then
icon_name, horizontal_shift = 'volume_mute', height_half * 0.25
elseif state.volume <= 60 then
icon_name, horizontal_shift = 'volume_down', height_half * 0.125
end
local underlay_opacity = {main = visibility * 0.3, border = visibility}
ass:icon(mute_rect.ax + width_half, mute_rect.ay + height_half, icon_size, 'volume_up',
{border = options.text_border * state.scale, opacity = underlay_opacity, align = 5}
)
ass:icon(mute_rect.ax + width_half - horizontal_shift, mute_rect.ay + height_half, icon_size, icon_name,
{opacity = visibility, align = 5}
)
return ass
end
return Volume

View File

@@ -5,18 +5,20 @@ local WindowBorder = class(Element)
function WindowBorder:new() return Class.new(self) --[[@as WindowBorder]] end
function WindowBorder:init()
Element.init(self, 'window_border')
self.ignores_menu = true
Element.init(self, 'window_border', {render_order = 9999})
self.size = 0
self:decide_enabled()
end
function WindowBorder:decide_enabled()
self.enabled = options.window_border_size > 0 and not state.fullormaxed and not state.border
self.size = self.enabled and options.window_border_size or 0
self.size = self.enabled and round(options.window_border_size * state.scale) or 0
end
function WindowBorder:on_prop_border() self:decide_enabled() end
function WindowBorder:on_prop_title_bar() self:decide_enabled() end
function WindowBorder:on_prop_fullormaxed() self:decide_enabled() end
function WindowBorder:on_options() self:decide_enabled() end
function WindowBorder:render()
if self.size > 0 then
@@ -24,7 +26,7 @@ function WindowBorder:render()
local clip = '\\iclip(' .. self.size .. ',' .. self.size .. ',' ..
(display.width - self.size) .. ',' .. (display.height - self.size) .. ')'
ass:rect(0, 0, display.width + 1, display.height + 1, {
color = bg, clip = clip, opacity = options.window_border_opacity,
color = bg, clip = clip, opacity = config.opacity.border,
})
return ass
end

83
src/uosc/intl/de.json Normal file
View File

@@ -0,0 +1,83 @@
{
"%s are empty": "%s sind leer",
"%s channel": "%s Kanal",
"%s channels": "%s Kanäle",
"%s to search": "%s um zu suchen",
"An error has occurred.": "Ein Fehler ist aufgetreten.",
"Aspect ratio": "Seitenverhältnis",
"Audio": "Audio",
"Audio device": "Audiogerät",
"Audio devices": "Audiogeräte",
"Audio tracks": "Audiospuren",
"Chapter %s": "Kapitel %s",
"Chapters": "Kapitel",
"Default": "Standard",
"Default %s": "Standard %s",
"Delete file & Next": "Lösche Datei & Nächstes",
"Delete file & Prev": "Lösche Datei & Vorheriges",
"Delete file & Quit": "Lösche Datei & Beenden",
"Disabled": "Deaktiviert",
"Download": "Herunterladen",
"Drives": "Laufwerke",
"Drop files or URLs to play here": "Dateien oder URLs zum Abspielen hier ablegen",
"Edition %s": "Edition %s",
"Editions": "Editionen",
"Empty": "Leer",
"First": "Erstes",
"Fullscreen": "Vollbild",
"Key bindings": "Tastenkürzel",
"Last": "Letztes",
"Load": "Hinzufügen",
"Load audio": "Audio hinzufügen",
"Load subtitles": "Untertitel hinzufügen",
"Load video": "Video hinzufügen",
"Loop file": "Datei wiederholen",
"Loop playlist": "Wiedergabeliste wiederholen",
"Menu": "Menü",
"Navigation": "Navigation",
"Next": "Nächstes",
"Next page": "Nächste Seite",
"No file": "Keine Datei",
"Open config folder": "Konfigurationsordner öffnen",
"Open file": "Datei öffnen",
"Play/Pause": "Abspielen/Pause",
"Playlist": "Wiedergabeliste",
"Playlist/Files": "Wiedergabeliste/Dateien",
"Prev": "Vorheriges",
"Previous": "Vorheriges",
"Previous page": "Vorherige Seite",
"Quit": "Beenden",
"Remaining downloads today: %s": "Verbleibende Downloads heute: %s",
"Resets in: %s": "Zurückgesetzt in: %s",
"Screenshot": "Bildschirmfoto",
"See above for clues.": "Siehe oben für Hinweise.",
"Show in directory": "Im Verzeichnis anzeigen",
"Shuffle": "Zufällig",
"Stream quality": "Streamqualität",
"Subtitles": "Untertitel",
"Subtitles loaded & enabled": "Untertitel geladen & aktiviert",
"Track %s": "Spur %s",
"Update uosc": "Aktualisiere uosc",
"Updating uosc": "uosc wird aktualisiert",
"Utils": "Werkzeuge",
"Video": "Video",
"default": "Standard",
"drive": "Laufwerk",
"enter query": "Anfrage eingeben",
"error": "Fehler",
"external": "extern",
"forced": "erzwungen",
"foreign parts only": "nur fremdsprachige Teile",
"hearing impaired": "Gehörgeschädigte",
"invalid response json (see console for details)": "Ungültige JSON-Antwort (siehe Konsole für Details)",
"no results": "Keine Ergebnisse",
"open file": "Datei öffnen",
"parent dir": "übergeordnetes Verzeichnis",
"playlist or file": "Wiedergabeliste oder Datei",
"process exited with code %s (see console for details)": "Prozess endete mit dem Status %s (siehe Konsole für Details)",
"search online": "Suche im Internet",
"type & ctrl+enter to search": "Tippe & Strg+Eingabe um zu suchen",
"type to search": "Tippe um zu suchen",
"unknown error": "Unbekannter Fehler",
"uosc has been installed. Restart mpv for it to take effect.": "uosc wurde installiert. mpv muss neu gestarted werden um es wirksam zu machen."
}

59
src/uosc/intl/es.json Normal file
View File

@@ -0,0 +1,59 @@
{
"Aspect ratio": "Relación de aspecto",
"Audio": "Audio",
"Audio device": "Dispositivo de audio",
"Audio devices": "Dispositivos de audio",
"Audio tracks": "Pistas de audio",
"Autoselect device": "Selección automática",
"Chapter %s": "Capítulo %s",
"Chapters": "Capítulos",
"Default": "Por defecto",
"Default %s": "Por defecto %s",
"Delete file & Next": "Eliminar archivo y siguiente",
"Delete file & Prev": "Eliminar archivo y anterior",
"Delete file & Quit": "Eliminar archivo y salir",
"Disabled": "Desactivado",
"Drives": "Unidades",
"Edition": "Edición",
"Edition %s": "Edición %s",
"Editions": "Ediciones",
"Empty": "Vacío",
"First": "Primero",
"Fullscreen": "Pantalla completa",
"Last": "Último",
"Load": "Abrir",
"Load audio": "Añadir una pista de audio",
"Load subtitles": "Añadir una pista de subtítulos",
"Load video": "Añadir una pista de vídeo",
"Loop file": "Repetir archivo",
"Loop playlist": "Repetir lista de reproducción",
"Menu": "Menú",
"Navigation": "Navegación",
"Next": "Siguiente",
"No file": "Ningún archivo",
"Open config folder": "Abrir carpeta de configuración",
"Open file": "Abrir un archivo",
"Playlist": "Lista de reproducción",
"Playlist/Files": "Lista de reproducción / Archivos",
"Prev": "Anterior",
"Previous": "Anterior",
"Quit": "Salir",
"Screenshot": "Captura de pantalla",
"Show in directory": "Acceder a la carpeta",
"Shuffle": "Reproducción aleatoria",
"Stream quality": "Calidad del flujo",
"Subtitles": "Subtítulos",
"Track": "Pista",
"Track %s": "Pista %s",
"Utils": "Utilidades",
"Video": "Vídeo",
"%s channel": "%s canal",
"%s channels": "%s canales",
"default": "por defecto",
"drive": "unidad",
"external": "externo",
"forced": "forzado",
"open file": "seleccionar un archivo",
"parent dir": "directorio padre",
"playlist or file": "archivo o lista de reproducción"
}

59
src/uosc/intl/fr.json Normal file
View File

@@ -0,0 +1,59 @@
{
"Aspect ratio": "Format d'image",
"Audio": "Audio",
"Audio device": "Périphérique audio",
"Audio devices": "Périphériques audio",
"Audio tracks": "Pistes audio",
"Autoselect device": "Sélection automatique",
"Chapter %s": "Chapitre %s",
"Chapters": "Chapitres",
"Default": "Par défaut",
"Default %s": "Par défaut %s",
"Delete file & Next": "Supprimer le fichier et Suivant",
"Delete file & Prev": "Supprimer le fichier et Précédent",
"Delete file & Quit": "Supprimer le fichier et Quitter",
"Disabled": "Désactivé",
"Drives": "Lecteurs",
"Edition": "Édition",
"Edition %s": "Édition %s",
"Editions": "Éditions",
"Empty": "Vide",
"First": "Premier",
"Fullscreen": "Plein écran",
"Last": "Dernier",
"Load": "Ouvrir",
"Load audio": "Ajouter une piste audio",
"Load subtitles": "Ajouter une piste de sous-titres",
"Load video": "Ajouter une piste vidéo",
"Loop file": "Lire en boucle le fichier",
"Loop playlist": "Lire en boucle la liste de lecture",
"Menu": "Menu",
"Navigation": "Navigation",
"Next": "Suivant",
"No file": "Aucun fichier",
"Open config folder": "Ouvrir le dossier de configuration",
"Open file": "Ouvrir un fichier",
"Playlist": "Liste de lecture",
"Playlist/Files": "Liste de lecture / Fichiers",
"Prev": "Précédent",
"Previous": "Précédent",
"Quit": "Quitter",
"Screenshot": "Capture d'écran",
"Show in directory": "Accéder au dossier",
"Shuffle": "Lecture aléatoire",
"Stream quality": "Qualité du flux",
"Subtitles": "Sous-titres",
"Track": "Piste",
"Track %s": "Piste %s",
"Utils": "Outils",
"Video": "Vidéo",
"%s channel": "%s canal",
"%s channels": "%s canaux",
"default": "par défaut",
"drive": "lecteur",
"external": "externe",
"forced": "forcé",
"open file": "sélectionner un fichier",
"parent dir": "répertoire parent",
"playlist or file": "fichier ou liste de lecture"
}

59
src/uosc/intl/ro.json Normal file
View File

@@ -0,0 +1,59 @@
{
"Aspect ratio": "Raportul de aspect",
"Audio": "Audio",
"Audio device": "Dispozitiv audio",
"Audio devices": "Dispozitive audio",
"Audio tracks": "Piese audio",
"Autoselect device": "Selectare automată",
"Chapter %s": "Capitolul %s",
"Chapters": "Capitole",
"Default": "Implicit",
"Default %s": "Implicit %s",
"Delete file & Next": "Ștergere fișier și următorul",
"Delete file & Prev": "Ștergere fișier și anteriorul",
"Delete file & Quit": "Ștergere fișier și ieși",
"Disabled": "Dezactivat",
"Drives": "Unități",
"Edition": "Ediție",
"Edition %s": "Ediție %s",
"Editions": "Ediții",
"Empty": "Gol",
"First": "Primul",
"Fullscreen": "Ecran complet",
"Last": "Ultimul",
"Load": "Încarcă",
"Load audio": "Deschide audio",
"Load subtitles": "Deschide subtitrările",
"Load video": "Deschide video",
"Loop file": "Repetă fișierul",
"Loop playlist": "Repetă lista de redare",
"Menu": "Meniu",
"Navigation": "Navigare",
"Next": "Următor",
"No file": "Niciun fisier",
"Open config folder": "Deschide dosarul de configurație",
"Open file": "Deschide fișierul",
"Playlist": "Listă de redare",
"Playlist/Files": "Listă de redare/Fișiere",
"Prev": "Precedent",
"Previous": "Precedent",
"Quit": "Ieșire",
"Screenshot": "Captură de ecran",
"Show in directory": "Arată în dosar",
"Shuffle": "Amestecă",
"Stream quality": "Calitatea fluxului",
"Subtitles": "Subtitrări",
"Track": "Pistă",
"Track %s": "Pistă %s",
"Utils": "Utilități",
"Video": "Video",
"%s channel": "%s canal",
"%s channels": "%s canale",
"default": "implicit",
"drive": "unitate",
"external": "extern",
"forced": "forțat",
"open file": "deschide fișierul",
"parent dir": "director părinte",
"playlist or file": "fișier sau listă de redare"
}

59
src/uosc/intl/ru.json Normal file
View File

@@ -0,0 +1,59 @@
{
"Aspect ratio": "Соотношение сторон",
"Audio": "Аудио",
"Audio device": "Аудиоустройство",
"Audio devices": "Аудиоустройства",
"Audio tracks": "Аудиодорожки",
"Autoselect device": "Автовыбор устройства",
"Chapter %s": "Глава %s",
"Chapters": "Главы",
"Default": "По умолчанию",
"Default %s": "По умолчанию %s",
"Delete file & Next": "Удалить файл и след.",
"Delete file & Prev": "Удалить файл и пред.",
"Delete file & Quit": "Удалить файл и выйти",
"Disabled": "Отключено",
"Drives": "Диски",
"Edition": "Редакция",
"Edition %s": "Редакция %s",
"Editions": "Редакции",
"Empty": "Пусто",
"First": "Первый",
"Fullscreen": "Полный экран",
"Last": "Последний",
"Load": "Загрузить",
"Load audio": "Загрузить аудио",
"Load subtitles": "Загрузить субтитры",
"Load video": "Загрузить видео",
"Loop file": "Повторять файл",
"Loop playlist": "Повторять плейлист",
"Menu": "Меню",
"Navigation": "Навигация",
"Next": "Следующий",
"No file": "Нет файла",
"Open config folder": "Открыть папку конфигурации",
"Open file": "Открыть файл",
"Playlist": "Плейлист",
"Playlist/Files": "Плейлист / файлы",
"Prev": "Предыдущий",
"Previous": "Предыдущий",
"Quit": "Выйти",
"Screenshot": "Скриншот",
"Show in directory": "Показать в папке",
"Shuffle": "Перемешать",
"Stream quality": "Качество потока",
"Subtitles": "Субтитры",
"Track": "Дорожка",
"Track %s": "Дорожка %s",
"Utils": "Инструменты",
"Video": "Видео",
"%s channels": "%s канала/-ов",
"%s channel": "%s канал",
"default": "по умолчанию",
"drive": "диск",
"external": "внешняя",
"forced": "форсированная",
"open file": "открыть файл",
"parent dir": "родительская папка",
"playlist or file": "плейлист или файл"
}

69
src/uosc/intl/uk.json Normal file
View File

@@ -0,0 +1,69 @@
{
"Aspect ratio": "Співвідношення сторін",
"Audio": "Аудіо",
"Audio device": "Аудіопристрій",
"Audio devices": "Аудіопристрої",
"Audio tracks": "Аудіодоріжки",
"Autoselect device": "Автовибір пристрою",
"Chapter %s": "Розділ %s",
"Chapters": "Розділи",
"Default": "За замовчуванням",
"Default %s": "За замовчуванням %s",
"Delete file & Next": "Видалити файл & Наступний",
"Delete file & Prev": "Видалити файл & Попередній",
"Delete file & Quit": "Видалити файл & Вийти",
"Disabled": "Вимкнено",
"Drives": "Диски",
"Edition": "Видання",
"Edition %s": "Видання %s",
"Editions": "Видання",
"Empty": "Порожньо",
"First": "Перший",
"Fullscreen": "На весь екран",
"Last": "Останній",
"Load": "Завантажити",
"Load audio": "Завантажити аудіо",
"Load subtitles": "Завантажити субтитри",
"Load video": "Завантажити відео",
"Loop file": "Повторювати файл",
"Loop playlist": "Повторювати плейліст",
"Menu": "Меню",
"Navigation": "Навігація",
"Next": "Наступний",
"No file": "Файл відсутній",
"Open config folder": "Відкрити каталог конфігурації",
"Open file": "Відкрити файл",
"Playlist": "Плейліст",
"Playlist/Files": "Плейліст / Файли",
"Prev": "Попередній",
"Previous": "Попередній",
"Quit": "Вийти",
"Screenshot": "Скриншот",
"Show in directory": "Показати в каталозі",
"Shuffle": "Перемішати",
"Stream quality": "Якість потоку",
"Subtitles": "Субтитри",
"Track": "Трек",
"Track %s": "Трек %s",
"Utils": "Інструменти",
"Video": "Відео",
"%s channels": "%s канали/-ів",
"%s channel": "%s канал",
"default": "за замовчуванням",
"drive": "диск",
"external": "зовнішня",
"forced": "примусова",
"open file": "відкрити файл",
"parent dir": "батьківський каталог",
"playlist or file": "плейліст або файл",
"type to search": "Введіть для пошуку",
"type & ctrl+enter to search": "Введіть & Ctrl+Enter для пошуку",
"Key bindings": "Комбінації клавіш",
"Drop files or URLs to play here": "Перемістіть файли або URL-адреси для відтворення сюди",
"Update uosc": "Оновити uosc",
"Updating uosc": "uosc оновлюється",
"uosc has been installed. Restart mpv for it to take effect.": "uosc встановлено. mpv потрібно перезапустити.",
"An error has occurred.": "Сталася помилка.",
"See above for clues.": "Дивіться підказки вище.",
"Play/Pause": "Відтворення / Пауза"
}

View File

@@ -0,0 +1,83 @@
{
"%s are empty": "%s 为空",
"%s channel": "%s 声道",
"%s channels": "%s 声道",
"%s to search": "%s 进行搜索",
"An error has occurred.": "出现错误",
"Aspect ratio": "纵横比",
"Audio": "音频",
"Audio device": "音频设备",
"Audio devices": "音频设备",
"Audio tracks": "音频轨道",
"Chapter %s": "第 %s 章",
"Chapters": "章节",
"Default": "默认",
"Default %s": "默认 %s",
"Delete file & Next": "删除文件并播放下一个",
"Delete file & Prev": "删除文件并播放上一个",
"Delete file & Quit": "删除文件并退出",
"Disabled": "禁用",
"Download": "下载",
"Drives": "驱动器",
"Drop files or URLs to play here": "拖放文件或 URLs 到此处进行播放",
"Edition %s": "版本 %s",
"Editions": "版本",
"Empty": "空",
"First": "第一个",
"Fullscreen": "全屏",
"Key bindings": "键位绑定",
"Last": "最后一个",
"Load": "加载",
"Load audio": "加载音频",
"Load subtitles": "加载字幕",
"Load video": "加载视频",
"Loop file": "单个循环",
"Loop playlist": "列表循环",
"Menu": "菜单",
"Navigation": "导航",
"Next": "下一个",
"Next page": "下一页",
"No file": "无文件",
"Open config folder": "打开设置文件夹",
"Open file": "打开文件",
"Play/Pause": "播放/暂停",
"Playlist": "播放列表",
"Playlist/Files": "播放/文件列表",
"Prev": "上一个",
"Previous": "上一个",
"Previous page": "上一页",
"Quit": "退出",
"Remaining downloads today: %s": "今天的剩余下载量: %s",
"Resets in: %s": "重置: %s",
"Screenshot": "截图",
"See above for clues.": "线索见上文",
"Show in directory": "打开所在文件夹",
"Shuffle": "乱序",
"Stream quality": "流媒体品质",
"Subtitles": "字幕",
"Subtitles loaded & enabled": "字幕已加载并启用",
"Track %s": "轨道 %s",
"Update uosc": "更新 uosc",
"Updating uosc": "正在更新 uosc",
"Utils": "工具",
"Video": "视频",
"default": "默认",
"drive": "磁盘",
"enter query": "输入查询",
"error": "错误",
"external": "外置",
"forced": "强制",
"foreign parts only": "仅限外语部分",
"hearing impaired": "听力障碍",
"invalid response json (see console for details)": "无效的响应 json (请参阅控制台了解详细信息)",
"no results": "没有结果",
"open file": "打开文件",
"parent dir": "父文件夹",
"playlist or file": "播放列表或文件",
"process exited with code %s (see console for details)": "进程以代码 %s 退出 (请参阅控制台了解详细信息)",
"search online": "在线搜索",
"type & ctrl+enter to search": "输入并按 ctrl+enter 进行搜索",
"type to search": "输入搜索内容",
"unknown error": "未知错误",
"uosc has been installed. Restart mpv for it to take effect.": "uosc 已经安装,重新启动 mpv 使其生效"
}

268
src/uosc/lib/ass.lua Normal file
View File

@@ -0,0 +1,268 @@
--[[ ASSDRAW EXTENSIONS ]]
local ass_mt = getmetatable(assdraw.ass_new())
-- Opacity.
---@param self table|nil
---@param opacity number|{primary?: number; border?: number, shadow?: number, main?: number} Opacity of all elements.
---@param fraction? number Optionally adjust the above opacity by this fraction.
---@return string|nil
function ass_mt.opacity(self, opacity, fraction)
fraction = fraction ~= nil and fraction or 1
opacity = type(opacity) == 'table' and opacity or {main = opacity}
local text = ''
if opacity.main then
text = text .. string.format('\\alpha&H%X&', opacity_to_alpha(opacity.main * fraction))
end
if opacity.primary then
text = text .. string.format('\\1a&H%X&', opacity_to_alpha(opacity.primary * fraction))
end
if opacity.border then
text = text .. string.format('\\3a&H%X&', opacity_to_alpha(opacity.border * fraction))
end
if opacity.shadow then
text = text .. string.format('\\4a&H%X&', opacity_to_alpha(opacity.shadow * fraction))
end
if self == nil then
return text
elseif text ~= '' then
self.text = self.text .. '{' .. text .. '}'
end
end
-- Icon.
---@param x number
---@param y number
---@param size number
---@param name string
---@param opts? {color?: string; border?: number; border_color?: string; opacity?: number; clip?: string; align?: number}
function ass_mt:icon(x, y, size, name, opts)
opts = opts or {}
opts.font, opts.size, opts.bold = 'MaterialIconsRound-Regular', size, false
self:txt(x, y, opts.align or 5, name, opts)
end
-- Text.
-- Named `txt` because `ass.text` is a value.
---@param x number
---@param y number
---@param align number
---@param value string|number
---@param opts {size: number; font?: string; color?: string; bold?: boolean; italic?: boolean; border?: number; border_color?: string; shadow?: number; shadow_color?: string; rotate?: number; wrap?: number; opacity?: number|{primary?: number; border?: number, shadow?: number, main?: number}; clip?: string}
function ass_mt:txt(x, y, align, value, opts)
local border_size = opts.border or 0
local shadow_size = opts.shadow or 0
local tags = '\\pos(' .. x .. ',' .. y .. ')\\rDefault\\an' .. align .. '\\blur0'
-- font
tags = tags .. '\\fn' .. (opts.font or config.font)
-- font size
tags = tags .. '\\fs' .. opts.size
-- bold
if opts.bold or (opts.bold == nil and options.font_bold) then tags = tags .. '\\b1' end
-- italic
if opts.italic then tags = tags .. '\\i1' end
-- rotate
if opts.rotate then tags = tags .. '\\frz' .. opts.rotate end
-- wrap
if opts.wrap then tags = tags .. '\\q' .. opts.wrap end
-- border
tags = tags .. '\\bord' .. border_size
-- shadow
tags = tags .. '\\shad' .. shadow_size
-- colors
tags = tags .. '\\1c&H' .. (opts.color or bgt)
if border_size > 0 then tags = tags .. '\\3c&H' .. (opts.border_color or bg) end
if shadow_size > 0 then tags = tags .. '\\4c&H' .. (opts.shadow_color or bg) end
-- opacity
if opts.opacity then tags = tags .. self.opacity(nil, opts.opacity) end
-- clip
if opts.clip then tags = tags .. opts.clip end
-- render
self:new_event()
self.text = self.text .. '{' .. tags .. '}' .. value
end
-- Tooltip.
---@param element Rect
---@param value string|number
---@param opts? {size?: number; align?: number; offset?: number; bold?: boolean; italic?: boolean; width_overwrite?: number, margin?: number; responsive?: boolean; lines?: integer, timestamp?: boolean; invert_colors?: boolean}
function ass_mt:tooltip(element, value, opts)
if value == '' then return end
opts = opts or {}
opts.size = opts.size or round(16 * state.scale)
opts.border = options.text_border * state.scale
opts.border_color = opts.invert_colors and fg or bg
opts.margin = opts.margin or round(10 * state.scale)
opts.lines = opts.lines or 1
opts.color = opts.invert_colors and bg or fg
local offset = opts.offset or 2
local padding_y = round(opts.size / 6)
local padding_x = round(opts.size / 3)
local width = (opts.width_overwrite or text_width(value, opts)) + padding_x * 2
local height = opts.size * opts.lines + 2 * padding_y
local width_half, height_half = width / 2, height / 2
local margin = opts.margin + Elements:v('window_border', 'size', 0)
local align = opts.align or 8
local x, y = 0, 0 -- center of tooltip
-- Flip alignment to other side when not enough space
if opts.responsive ~= false then
if align == 8 then
if element.ay - offset - height < margin then align = 2 end
elseif align == 2 then
if element.by + offset + height > display.height - margin then align = 8 end
elseif align == 6 then
if element.bx + offset + width > display.width - margin then align = 4 end
elseif align == 4 then
if element.ax - offset - width < margin then align = 6 end
end
end
-- Calculate tooltip center based on alignment
if align == 8 or align == 2 then
x = clamp(margin + width_half, element.ax + (element.bx - element.ax) / 2, display.width - margin - width_half)
y = align == 8 and element.ay - offset - height_half or element.by + offset + height_half
else
x = align == 6 and element.bx + offset + width_half or element.ax - offset - width_half
y = clamp(margin + height_half, element.ay + (element.by - element.ay) / 2, display.height - margin - height_half)
end
-- Draw
local ax, ay, bx, by = round(x - width_half), round(y - height_half), round(x + width_half), round(y + height_half)
self:rect(ax, ay, bx, by, {
color = opts.invert_colors and fg or bg, opacity = config.opacity.tooltip, radius = state.radius
})
local func = opts.timestamp and self.timestamp or self.txt
func(self, x, y, 5, tostring(value), opts)
return {ax = element.ax, ay = ay, bx = element.bx, by = by}
end
-- Timestamp with each digit positioned as if it was replaced with 0
---@param x number
---@param y number
---@param align number
---@param timestamp string
---@param opts {size: number; opacity?: number|{primary?: number; border?: number, shadow?: number, main?: number}}
function ass_mt:timestamp(x, y, align, timestamp, opts)
local widths, width_total = {}, 0
zero_rep = timestamp_zero_rep(timestamp)
for i = 1, #zero_rep do
local width = text_width(zero_rep:sub(i, i), opts)
widths[i] = width
width_total = width_total + width
end
-- shift x and y to fit align 5
local mod_align = align % 3
if mod_align == 0 then
x = x - width_total
elseif mod_align == 2 then
x = x - width_total / 2
end
if align < 4 then
y = y - opts.size / 2
elseif align > 6 then
y = y + opts.size / 2
end
local opacity = opts.opacity
local primary_opacity
if type(opacity) == 'table' then
opts.opacity = {main = opacity.main, border = opacity.border, shadow = opacity.shadow, primary = 0}
primary_opacity = opacity.primary or opacity.main
else
opts.opacity = {main = opacity, primary = 0}
primary_opacity = opacity
end
for i, width in ipairs(widths) do
self:txt(x + width / 2, y, 5, timestamp:sub(i, i), opts)
x = x + width
end
x = x - width_total
opts.opacity = {main = 0, primary = primary_opacity or 1}
for i, width in ipairs(widths) do
self:txt(x + width / 2, y, 5, timestamp:sub(i, i), opts)
x = x + width
end
opts.opacity = opacity
end
-- Rectangle.
---@param ax number
---@param ay number
---@param bx number
---@param by number
---@param opts? {color?: string; border?: number; border_color?: string; opacity?: number|{primary?: number; border?: number, shadow?: number, main?: number}; clip?: string, radius?: number}
function ass_mt:rect(ax, ay, bx, by, opts)
opts = opts or {}
local border_size = opts.border or 0
local tags = '\\pos(0,0)\\rDefault\\an7\\blur0'
-- border
tags = tags .. '\\bord' .. border_size
-- colors
tags = tags .. '\\1c&H' .. (opts.color or fg)
if border_size > 0 then tags = tags .. '\\3c&H' .. (opts.border_color or bg) end
-- opacity
if opts.opacity then tags = tags .. self.opacity(nil, opts.opacity) end
-- clip
if opts.clip then
tags = tags .. opts.clip
end
-- draw
self:new_event()
self.text = self.text .. '{' .. tags .. '}'
self:draw_start()
if opts.radius and opts.radius > 0 then
self:round_rect_cw(ax, ay, bx, by, opts.radius)
else
self:rect_cw(ax, ay, bx, by)
end
self:draw_stop()
end
-- Circle.
---@param x number
---@param y number
---@param radius number
---@param opts? {color?: string; border?: number; border_color?: string; opacity?: number; clip?: string}
function ass_mt:circle(x, y, radius, opts)
opts = opts or {}
opts.radius = radius
self:rect(x - radius, y - radius, x + radius, y + radius, opts)
end
-- Texture.
---@param ax number
---@param ay number
---@param bx number
---@param by number
---@param char string Texture font character.
---@param opts {size?: number; color: string; opacity?: number; clip?: string; anchor_x?: number, anchor_y?: number}
function ass_mt:texture(ax, ay, bx, by, char, opts)
opts = opts or {}
local anchor_x, anchor_y = opts.anchor_x or ax, opts.anchor_y or ay
local clip = opts.clip or ('\\clip(' .. ax .. ',' .. ay .. ',' .. bx .. ',' .. by .. ')')
local tile_size, opacity = opts.size or 100, opts.opacity or 0.2
local x, y = ax - (ax - anchor_x) % tile_size, ay - (ay - anchor_y) % tile_size
local width, height = bx - x, by - y
local line = string.rep(char, math.ceil((width / tile_size)))
local lines = ''
for i = 1, math.ceil(height / tile_size), 1 do lines = lines .. (lines == '' and '' or '\\N') .. line end
self:txt(
x, y, 7, lines,
{font = 'uosc_textures', size = tile_size, color = opts.color, bold = false, opacity = opacity, clip = clip})
end
-- Rotating spinner icon.
---@param x number
---@param y number
---@param size number
---@param opts? {color?: string; opacity?: number; clip?: string; border?: number; border_color?: string;}
function ass_mt:spinner(x, y, size, opts)
opts = opts or {}
opts.rotate = (state.render_last_time * 1.75 % 1) * -360
opts.color = opts.color or fg
self:icon(x, y, size, 'autorenew', opts)
request_render()
end

69
src/uosc/lib/buttons.lua Normal file
View File

@@ -0,0 +1,69 @@
---@alias ButtonData {icon: string; active?: boolean; badge?: string; command?: string | string[]; tooltip?: string;}
---@alias ButtonSubscriber fun(data: ButtonData)
local buttons = {
---@type ButtonData[]
data = {},
---@type table<string, ButtonSubscriber[]>
subscribers = {},
}
---@param name string
---@param callback fun(data: ButtonData)
function buttons:subscribe(name, callback)
local pool = self.subscribers[name]
if not pool then
pool = {}
self.subscribers[name] = pool
end
pool[#pool + 1] = callback
self:trigger(name)
return function() buttons:unsubscribe(name, callback) end
end
---@param name string
---@param callback? ButtonSubscriber
function buttons:unsubscribe(name, callback)
if self.subscribers[name] then
if callback == nil then
self.subscribers[name] = {}
else
itable_delete_value(self.subscribers[name], callback)
end
end
end
---@param name string
function buttons:trigger(name)
local pool = self.subscribers[name]
local data = self.data[name] or {icon = 'help_center', tooltip = 'Uninitialized button "' .. name .. '"'}
if pool then
for _, callback in ipairs(pool) do callback(data) end
end
end
---@param name string
---@param data ButtonData
function buttons:set(name, data)
buttons.data[name] = data
buttons:trigger(name)
request_render()
end
mp.register_script_message('set-button', function(name, data)
if type(name) ~= 'string' then
msg.error('Invalid set-button message parameter: 1st parameter (name) has to be a string.')
return
end
if type(data) ~= 'string' then
msg.error('Invalid set-button message parameter: 2nd parameter (data) has to be a string.')
return
end
local data = utils.parse_json(data)
if type(data) == 'table' and type(data.icon) == 'string' then
buttons:set(name, data)
end
end)
return buttons

View File

@@ -0,0 +1,67 @@
require('lib/text')
local char_dir = mp.get_script_directory() .. '/char-conv/'
local data = {}
local languages = get_languages()
for _, lang in ipairs(languages) do
table_assign(data, get_locale_from_json(char_dir .. lang:lower() .. '.json'))
end
local romanization = {}
local function get_romanization_table()
for k, v in pairs(data) do
for _, char in utf8_iter(v) do
romanization[char] = k
end
end
end
get_romanization_table()
function need_romanization()
return next(romanization) ~= nil
end
function char_conv(chars, use_ligature, has_separator)
local separator = has_separator or ' '
local length = 0
local char_conv, sp, cache = {}, {}, {}
local chars_length = utf8_length(chars)
local concat = table.concat
for _, char in utf8_iter(chars) do
if use_ligature then
if #char == 1 then
char_conv[#char_conv + 1] = char
else
char_conv[#char_conv + 1] = romanization[char] or char
end
else
length = length + 1
if #char <= 2 then
if (char ~= ' ' and length ~= chars_length) then
cache[#cache + 1] = romanization[char] or char
elseif (char == ' ' or length == chars_length) then
if length == chars_length then
cache[#cache + 1] = romanization[char] or char
end
sp[#sp + 1] = concat(cache)
itable_clear(cache)
end
else
if next(cache) ~= nil then
sp[#sp + 1] = concat(cache)
itable_clear(cache)
end
sp[#sp + 1] = romanization[char] or char
end
end
end
if use_ligature then
return concat(char_conv)
else
return concat(sp, separator)
end
end
return char_conv

410
src/uosc/lib/cursor.lua Normal file
View File

@@ -0,0 +1,410 @@
local cursor = {
x = math.huge,
y = math.huge,
hidden = true,
hover_raw = false,
-- Event handlers that are only fired on zones defined during render loop.
---@type {event: string, hitbox: Hitbox; handler: fun(...)}[]
zones = {},
handlers = {
primary_down = {},
primary_up = {},
secondary_down = {},
secondary_up = {},
wheel_down = {},
wheel_up = {},
move = {},
},
first_real_mouse_move_received = false,
history = CircularBuffer:new(10),
autohide_fs_only = nil,
-- Tracks current key binding levels for each event. 0: disabled, 1: enabled, 2: enabled + window dragging prevented
binding_levels = {
mbtn_left = 0,
mbtn_left_dbl = 0,
mbtn_right = 0,
wheel = 0,
},
is_dragging_prevented = false,
event_forward_map = {
primary_down = 'MBTN_LEFT',
primary_up = 'MBTN_LEFT',
secondary_down = 'MBTN_RIGHT',
secondary_up = 'MBTN_RIGHT',
wheel_down = 'WHEEL_DOWN',
wheel_up = 'WHEEL_UP',
},
event_binding_map = {
primary_down = 'mbtn_left',
primary_up = 'mbtn_left',
primary_click = 'mbtn_left',
secondary_down = 'mbtn_right',
secondary_up = 'mbtn_right',
secondary_click = 'mbtn_right',
wheel_down = 'wheel',
wheel_up = 'wheel',
},
window_dragging_blockers = create_set({'primary_click', 'primary_down'}),
event_propagation_blockers = {
primary_down = 'primary_click',
primary_click = 'primary_down',
secondary_down = 'secondary_click',
secondary_click = 'secondary_down',
},
event_parent_map = {
primary_down = {is_start = true, trigger_event = 'primary_click'},
primary_up = {is_end = true, start_event = 'primary_down', trigger_event = 'primary_click'},
secondary_down = {is_start = true, trigger_event = 'secondary_click'},
secondary_up = {is_end = true, start_event = 'secondary_down', trigger_event = 'secondary_click'},
},
-- Holds positions of last events.
---@type {[string]: {x: number, y: number, time: number}}
last_event = {},
}
cursor.autohide_timer = mp.add_timeout(1, function() cursor:autohide() end)
cursor.autohide_timer:kill()
mp.observe_property('cursor-autohide', 'number', function(_, val)
cursor.autohide_timer.timeout = (val or 1000) / 1000
end)
-- Called at the beginning of each render
function cursor:clear_zones()
itable_clear(self.zones)
end
---@param hitbox Hitbox
function cursor:collides_with(hitbox)
return point_collides_with(self, hitbox)
end
-- Returns zone for event at current cursor position.
---@param event string
function cursor:find_zone(event)
-- Premature optimization to ignore a high frequency event that is not needed as a zone atm.
if event == 'move' then return end
for i = #self.zones, 1, -1 do
local zone = self.zones[i]
local is_blocking_only = zone.event == self.event_propagation_blockers[event]
if (zone.event == event or is_blocking_only) and self:collides_with(zone.hitbox) then
return not is_blocking_only and zone or nil
end
end
end
-- Defines an event zone for a hitbox on currently rendered screen. Available events:
-- - primary_down, primary_up, primary_click, secondary_down, secondary_up, secondary_click, wheel_down, wheel_up
--
-- Notes:
-- - Zones are cleared on beginning of every `render()`, and need to be rebound.
-- - One event type per zone: only the last bound zone per event gets triggered.
-- - In current implementation, you have to choose between `_click` or `_down`. Binding both makes only the last bound fire.
-- - Primary `_down` and `_click` disable dragging. Define `window_drag = true` on hitbox to re-enable.
-- - Anything that disables dragging also implicitly disables cursor autohide.
-- - `move` event zones are ignored due to it being a high frequency event that is currently not needed as a zone.
---@param event string
---@param hitbox Hitbox
---@param callback fun(...)
function cursor:zone(event, hitbox, callback)
self.zones[#self.zones + 1] = {event = event, hitbox = hitbox, handler = callback}
end
-- Binds a permanent cursor event handler active until manually unbound using `cursor:off()`.
-- `_click` events are not available as permanent global events, only as zones.
---@param event string
---@return fun() disposer Unbinds the event.
function cursor:on(event, callback)
if self.handlers[event] and not itable_index_of(self.handlers[event], callback) then
self.handlers[event][#self.handlers[event] + 1] = callback
self:decide_keybinds()
end
return function() self:off(event, callback) end
end
-- Unbinds a cursor event handler.
---@param event string
function cursor:off(event, callback)
if self.handlers[event] then
local index = itable_index_of(self.handlers[event], callback)
if index then
table.remove(self.handlers[event], index)
self:decide_keybinds()
end
end
end
-- Binds a cursor event handler to be called once.
---@param event string
function cursor:once(event, callback)
local function callback_wrap()
callback()
self:off(event, callback_wrap)
end
return self:on(event, callback_wrap)
end
-- Trigger the event.
---@param event string
function cursor:trigger(event, ...)
local forward = true
-- Call raw event handlers.
local zone = self:find_zone(event)
local callbacks = self.handlers[event]
if zone or #callbacks > 0 then
forward = false
if zone then zone.handler(...) end
for _, callback in ipairs(callbacks) do callback(...) end
end
-- Call compound/parent (click) event handlers if both start and end events are within `parent_zone.hitbox`.
local parent = self.event_parent_map[event]
if parent then
local parent_zone = self:find_zone(parent.trigger_event)
if parent_zone then
forward = false -- Canceled here so we don't forward down events if they can lead to a click.
if parent.is_end then
local last_start_event = self.last_event[parent.start_event]
if last_start_event and point_collides_with(last_start_event, parent_zone.hitbox) then
parent_zone.handler(...)
end
end
end
end
-- Forward unhandled events.
if forward then
local forward_name = self.event_forward_map[event]
if forward_name then
-- Forward events if there was no handler.
local active = find_active_keybindings(forward_name)
if active then
local is_wheel = event:find('wheel', 1, true)
local is_up = event:sub(-3) == '_up'
if active.owner then
-- Binding belongs to other script, so make it look like regular key event.
-- Mouse bindings are simple, other keys would require repeat and pressed handling,
-- which can't be done with mp.set_key_bindings(), but is possible with mp.add_key_binding().
local state = is_wheel and 'pm' or is_up and 'um' or 'dm'
local name = active.cmd:sub(active.cmd:find('/') + 1, -1)
mp.commandv('script-message-to', active.owner, 'key-binding', name, state, forward_name)
elseif is_wheel or is_up then
-- input.conf binding, react to button release for mouse buttons
mp.command(active.cmd)
end
end
end
end
-- Update last event position.
local last = self.last_event[event] or {}
last.x, last.y, last.time = self.x, self.y, mp.get_time()
self.last_event[event] = last
-- Refresh cursor autohide timer.
self:queue_autohide()
end
-- Enables or disables keybinding groups based on what event listeners are bound.
function cursor:decide_keybinds()
local new_levels = {mbtn_left = 0, mbtn_right = 0, wheel = 0}
self.is_dragging_prevented = false
-- Check global events.
for name, handlers in ipairs(self.handlers) do
local binding = self.event_binding_map[name]
if binding then
new_levels[binding] = #handlers > 0 and 1 or 0
end
end
-- Check zones.
for _, zone in ipairs(self.zones) do
local binding = self.event_binding_map[zone.event]
if binding and cursor:collides_with(zone.hitbox) then
local new_level = (self.window_dragging_blockers[zone.event] and zone.hitbox.window_drag ~= true) and 2
or math.max(new_levels[binding], zone.hitbox.window_drag == false and 2 or 1)
new_levels[binding] = new_level
if new_level > 1 then
self.is_dragging_prevented = true
end
end
end
-- Window dragging only gets prevented when on top of an element, which is when double clicks should be ignored.
new_levels.mbtn_left_dbl = new_levels.mbtn_left == 2 and 2 or 0
for name, level in pairs(new_levels) do
if level ~= self.binding_levels[name] then
local flags = level == 1 and 'allow-vo-dragging+allow-hide-cursor' or ''
mp[(level == 0 and 'disable' or 'enable') .. '_key_bindings'](name, flags)
self.binding_levels[name] = level
self:queue_autohide()
end
end
end
function cursor:_find_history_sample()
local time = mp.get_time()
for _, e in self.history:iter_rev() do
if time - e.time > 0.1 then
return e
end
end
return self.history:tail()
end
-- Returns a table with current velocities in in pixels per second.
---@return Point
function cursor:get_velocity()
local snap = self:_find_history_sample()
if snap then
local x, y, time = self.x - snap.x, self.y - snap.y, mp.get_time()
local time_diff = time - snap.time
if time_diff > 0.001 then
return {x = x / time_diff, y = y / time_diff}
end
end
return {x = 0, y = 0}
end
---@param x integer
---@param y integer
function cursor:move(x, y)
local old_x, old_y = self.x, self.y
-- mpv reports initial mouse position on linux as (0, 0), which always
-- displays the top bar, so we hardcode cursor position as infinity until
-- we receive a first real mouse move event with coordinates other than 0,0.
if not self.first_real_mouse_move_received then
if x > 0 and y > 0 then
self.first_real_mouse_move_received = true
else
x, y = math.huge, math.huge
end
end
-- Add 0.5 to be in the middle of the pixel
self.x, self.y = x + 0.5, y + 0.5
if old_x ~= self.x or old_y ~= self.y then
if self.x == math.huge or self.y == math.huge then
self.hidden = true
self.history:clear()
-- Slowly fadeout elements that are currently visible
for _, id in ipairs(config.cursor_leave_fadeout_elements) do
local element = Elements[id]
if element then
local visibility = element:get_visibility()
if visibility > 0 then
element:tween_property('forced_visibility', visibility, 0, function()
element.forced_visibility = nil
end)
end
end
end
Elements:update_proximities()
Elements:trigger('global_mouse_leave')
else
if self.hidden then
-- Cancel potential fadeouts
for _, id in ipairs(config.cursor_leave_fadeout_elements) do
if Elements[id] then Elements[id]:tween_stop() end
end
self.hidden = false
Elements:trigger('global_mouse_enter')
end
Elements:update_proximities()
-- Update history
self.history:insert({x = self.x, y = self.y, time = mp.get_time()})
end
Elements:proximity_trigger('mouse_move')
self:queue_autohide()
end
self:trigger('move')
request_render()
end
function cursor:leave() self:move(math.huge, math.huge) end
function cursor:is_autohide_allowed()
return options.autohide and (not self.autohide_fs_only or state.fullscreen)
and not self.is_dragging_prevented
and not Menu:is_open()
end
mp.observe_property('cursor-autohide-fs-only', 'bool', function(_, val) cursor.autohide_fs_only = val end)
-- Cursor auto-hiding after period of inactivity.
function cursor:autohide()
if self:is_autohide_allowed() then
self:leave()
self.autohide_timer:kill()
end
end
function cursor:queue_autohide()
if self:is_autohide_allowed() then
self.autohide_timer:kill()
self.autohide_timer:resume()
end
end
-- Calculates distance in which cursor reaches rectangle if it continues moving on the same path.
-- Returns `nil` if cursor is not moving towards the rectangle.
---@param rect Rect
function cursor:direction_to_rectangle_distance(rect)
local prev = self:_find_history_sample()
if not prev then return false end
local end_x, end_y = self.x + (self.x - prev.x) * 1e10, self.y + (self.y - prev.y) * 1e10
return get_ray_to_rectangle_distance(self.x, self.y, end_x, end_y, rect)
end
function cursor:create_handler(event, cb)
return function(...)
call_maybe(cb, ...)
self:trigger(event, ...)
end
end
-- Movement
function handle_mouse_pos(_, mouse)
if not mouse then return end
if cursor.hover_raw and not mouse.hover then
cursor:leave()
else
cursor:move(mouse.x, mouse.y)
end
cursor.hover_raw = mouse.hover
end
mp.observe_property('mouse-pos', 'native', handle_mouse_pos)
-- Key binding groups
mp.set_key_bindings({
{
'mbtn_left',
cursor:create_handler('primary_up'),
cursor:create_handler('primary_down', function(...)
handle_mouse_pos(nil, mp.get_property_native('mouse-pos'))
end),
},
}, 'mbtn_left', 'force')
mp.set_key_bindings({
{'mbtn_left_dbl', 'ignore'},
}, 'mbtn_left_dbl', 'force')
mp.set_key_bindings({
{'mbtn_right', cursor:create_handler('secondary_up'), cursor:create_handler('secondary_down')},
}, 'mbtn_right', 'force')
mp.set_key_bindings({
{'wheel_up', cursor:create_handler('wheel_up')},
{'wheel_down', cursor:create_handler('wheel_down')},
}, 'wheel', 'force')
return cursor

View File

@@ -6,14 +6,14 @@ local cache = {}
function get_languages()
local languages = {}
for _, lang in ipairs(split(options.languages, ',')) do
for _, lang in ipairs(comma_split(options.languages)) do
if (lang == 'slang') then
local slang = mp.get_property_native('slang')
if slang then
itable_append(languages, slang)
end
else
itable_append(languages, { lang })
languages[#languages +1] = lang
end
end
@@ -22,28 +22,23 @@ end
---@param path string
function get_locale_from_json(path)
local expand_path = mp.command_native({ 'expand-path', path })
local expand_path = mp.command_native({'expand-path', path})
local meta, meta_error = utils.file_info(expand_path)
if not meta or not meta.is_file then
return {}
return nil
end
local json_file = io.open(expand_path, 'r')
if not json_file then
return {}
return nil
end
local json = json_file:read('*all')
json_file:close()
return utils.parse_json(json)
end
function make_locale()
local translations = {}
return translations
local json_table = utils.parse_json(json)
return json_table
end
---@param text string

1006
src/uosc/lib/menus.lua Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -17,6 +17,11 @@ function serialize_rgba(rgba)
}
end
-- Trim any white space from the start and end of the string.
---@param str string
---@return string
function trim(str) return str:match('^%s*(.-)%s*$') end
-- Trim any `char` from the end of the string.
---@param str string
---@param char string
@@ -52,6 +57,16 @@ function split(str, pattern)
return list
end
-- Handles common option and message inputs that need to be split by comma when strings.
---@param input string|string[]|nil
---@return string[]
function comma_split(input)
if not input then return {} end
if type(input) == 'table' then return itable_map(input, tostring) end
local str = tostring(input)
return str:match('^%s*$') and {} or split(str, ' *, *')
end
-- Get index of the last appearance of `sub` in `str`.
---@param str string
---@param sub string
@@ -83,7 +98,7 @@ function itable_has(itable, value)
end
---@param itable table
---@param compare fun(value: any, index: number)
---@param compare fun(value: any, index: number): boolean|integer|string|nil
---@param from? number Where to start search, defaults to `1`.
---@param to? number Where to end search, defaults to `#itable`.
---@return number|nil index
@@ -98,7 +113,7 @@ function itable_find(itable, compare, from, to)
end
---@param itable table
---@param decider fun(value: any, index: number)
---@param decider fun(value: any, index: number): boolean|integer|string|nil
function itable_filter(itable, decider)
local filtered = {}
for index, value in ipairs(itable) do
@@ -146,13 +161,13 @@ function itable_slice(itable, start_pos, end_pos)
end
---@generic T
---@param a T[]|nil
---@param b T[]|nil
---@param ...T[]|nil
---@return T[]
function itable_join(a, b)
local result = {}
if a then for _, value in ipairs(a) do result[#result + 1] = value end end
if b then for _, value in ipairs(b) do result[#result + 1] = value end end
function itable_join(...)
local args, result = {...}, {}
for i = 1, select('#', ...) do
if args[i] then for _, value in ipairs(args[i]) do result[#result + 1] = value end end
end
return result
end
@@ -163,24 +178,87 @@ function itable_append(target, source)
return target
end
---@param target any[]
---@param source any[]
---@param props? string[]
function table_assign(target, source, props)
if props then
for _, name in ipairs(props) do target[name] = source[name] end
else
for prop, value in pairs(source) do target[prop] = value end
function itable_clear(itable)
for i = #itable, 1, -1 do itable[i] = nil end
end
---@generic T
---@param input table<T, any>
---@return T[]
function table_keys(input)
local keys = {}
for key, _ in pairs(input) do keys[#keys + 1] = key end
return keys
end
---@generic T
---@param input table<any, T>
---@return T[]
function table_values(input)
local values = {}
for _, value in pairs(input) do values[#values + 1] = value end
return values
end
---@generic T: table<any, any>
---@param target T
---@param ... T|nil
---@return T
function table_assign(target, ...)
local args = {...}
for i = 1, select('#', ...) do
if type(args[i]) == 'table' then for key, value in pairs(args[i]) do target[key] = value end end
end
return target
end
---@generic T
---@param table T
---@generic T: table<any, any>
---@param target T
---@param source T
---@param props string[]
---@return T
function table_shallow_copy(table)
function table_assign_props(target, source, props)
for _, name in ipairs(props) do target[name] = source[name] end
return target
end
-- Assign props from `source` to `target` that are not in `props` set.
---@generic T: table<any, any>
---@param target T
---@param source T
---@param props table<string, boolean>
---@return T
function table_assign_exclude(target, source, props)
for key, value in pairs(source) do
if not props[key] then target[key] = value end
end
return target
end
-- `table_assign({}, input)` without loosing types :(
---@generic T: table<any, any>
---@param input T
---@return T
function table_copy(input) return table_assign({}, input) end
-- Converts itable values into `table<value, true>` map.
---@param values any[]
function create_set(values)
local result = {}
for key, value in pairs(table) do result[key] = value end
for _, value in ipairs(values) do result[value] = true end
return result
end
---@generic T: any
---@param input string
---@param value_sanitizer? fun(value: string, key: string): T
---@return table<string, T>
function serialize_key_value_list(input, value_sanitizer)
local result, sanitize = {}, value_sanitizer or function(value) return value end
for _, key_value_pair in ipairs(comma_split(input)) do
local key, value = key_value_pair:match('^([%w_]+)=([%w%.]+)$')
if key and value then result[key] = sanitize(value, key) end
end
return result
end
@@ -198,7 +276,60 @@ function Class:new(...)
object:init(...)
return object
end
function Class:init() end
function Class:init(...) end
function Class:destroy() end
function class(parent) return setmetatable({}, {__index = parent or Class}) end
---@class CircularBuffer<T> : Class
CircularBuffer = class()
function CircularBuffer:new(max_size) return Class.new(self, max_size) --[[@as CircularBuffer]] end
function CircularBuffer:init(max_size)
self.max_size = max_size
self.pos = 0
self.data = {}
end
function CircularBuffer:insert(item)
self.pos = self.pos % self.max_size + 1
self.data[self.pos] = item
end
function CircularBuffer:get(i)
return i <= #self.data and self.data[(self.pos + i - 1) % #self.data + 1] or nil
end
local function iter(self, i)
if i == #self.data then return nil end
i = i + 1
return i, self:get(i)
end
function CircularBuffer:iter()
return iter, self, 0
end
local function iter_rev(self, i)
if i == 1 then return nil end
i = i - 1
return i, self:get(i)
end
function CircularBuffer:iter_rev()
return iter_rev, self, #self.data + 1
end
function CircularBuffer:head()
return self.data[self.pos]
end
function CircularBuffer:tail()
if #self.data < 1 then return nil end
return self.data[self.pos % #self.data + 1]
end
function CircularBuffer:clear()
itable_clear(self.data)
self.pos = 0
end

View File

@@ -3,33 +3,33 @@
---@type CodePointRange[]
local zero_width_blocks = {
{0x0000, 0x001F}, -- C0
{0x007F, 0x009F}, -- Delete + C1
{0x034F, 0x034F}, -- combining grapheme joiner
{0x061C, 0x061C}, -- Arabic Letter Strong
{0x200B, 0x200F}, -- {zero-width space, zero-width non-joiner, zero-width joiner, left-to-right mark, right-to-left mark}
{0x2028, 0x202E}, -- {line separator, paragraph separator, Left-to-Right Embedding, Right-to-Left Embedding, Pop Directional Format, Left-to-Right Override, Right-to-Left Override}
{0x2060, 0x2060}, -- word joiner
{0x2066, 0x2069}, -- {Left-to-Right Isolate, Right-to-Left Isolate, First Strong Isolate, Pop Directional Isolate}
{0xFEFF, 0xFEFF}, -- zero-width non-breaking space
{0x0000, 0x001F}, -- C0
{0x007F, 0x009F}, -- Delete + C1
{0x034F, 0x034F}, -- combining grapheme joiner
{0x061C, 0x061C}, -- Arabic Letter Strong
{0x200B, 0x200F}, -- {zero-width space, zero-width non-joiner, zero-width joiner, left-to-right mark, right-to-left mark}
{0x2028, 0x202E}, -- {line separator, paragraph separator, Left-to-Right Embedding, Right-to-Left Embedding, Pop Directional Format, Left-to-Right Override, Right-to-Left Override}
{0x2060, 0x2060}, -- word joiner
{0x2066, 0x2069}, -- {Left-to-Right Isolate, Right-to-Left Isolate, First Strong Isolate, Pop Directional Isolate}
{0xFEFF, 0xFEFF}, -- zero-width non-breaking space
-- Some other characters can also be combined https://en.wikipedia.org/wiki/Combining_character
{0x0300, 0x036F}, -- Combining Diacritical Marks 0 BMP Inherited
{0x1AB0, 0x1AFF}, -- Combining Diacritical Marks Extended 0 BMP Inherited
{0x1DC0, 0x1DFF}, -- Combining Diacritical Marks Supplement 0 BMP Inherited
{0x20D0, 0x20FF}, -- Combining Diacritical Marks for Symbols 0 BMP Inherited
{0xFE20, 0xFE2F}, -- Combining Half Marks 0 BMP Cyrillic (2 characters), Inherited (14 characters)
{0x0300, 0x036F}, -- Combining Diacritical Marks 0 BMP Inherited
{0x1AB0, 0x1AFF}, -- Combining Diacritical Marks Extended 0 BMP Inherited
{0x1DC0, 0x1DFF}, -- Combining Diacritical Marks Supplement 0 BMP Inherited
{0x20D0, 0x20FF}, -- Combining Diacritical Marks for Symbols 0 BMP Inherited
{0xFE20, 0xFE2F}, -- Combining Half Marks 0 BMP Cyrillic (2 characters), Inherited (14 characters)
-- Egyptian Hieroglyph Format Controls and Shorthand format Controls
{0x13430, 0x1345F}, -- Egyptian Hieroglyph Format Controls 1 SMP Egyptian Hieroglyphs
{0x1BCA0, 0x1BCAF}, -- Shorthand Format Controls 1 SMP Common
-- not sure how to deal with those https://en.wikipedia.org/wiki/Spacing_Modifier_Letters
{0x02B0, 0x02FF}, -- Spacing Modifier Letters 0 BMP Bopomofo (2 characters), Latin (14 characters), Common (64 characters)
{0x02B0, 0x02FF}, -- Spacing Modifier Letters 0 BMP Bopomofo (2 characters), Latin (14 characters), Common (64 characters)
}
-- All characters have the same width as the first one
---@type CodePointRange[]
local same_width_blocks = {
{0x3400, 0x4DBF}, -- CJK Unified Ideographs Extension A 0 BMP Han
{0x4E00, 0x9FFF}, -- CJK Unified Ideographs 0 BMP Han
{0x3400, 0x4DBF}, -- CJK Unified Ideographs Extension A 0 BMP Han
{0x4E00, 0x9FFF}, -- CJK Unified Ideographs 0 BMP Han
{0x20000, 0x2A6DF}, -- CJK Unified Ideographs Extension B 2 SIP Han
{0x2A700, 0x2B73F}, -- CJK Unified Ideographs Extension C 2 SIP Han
{0x2B740, 0x2B81F}, -- CJK Unified Ideographs Extension D 2 SIP Han
@@ -52,18 +52,24 @@ local osd_width, osd_height = 100, 100
local function utf8_char_bytes(str, i)
local char_byte = str:byte(i)
local max_bytes = #str - i + 1
if char_byte < 0xC0 then return math.min(max_bytes, 1)
elseif char_byte < 0xE0 then return math.min(max_bytes, 2)
elseif char_byte < 0xF0 then return math.min(max_bytes, 3)
elseif char_byte < 0xF8 then return math.min(max_bytes, 4)
else return math.min(max_bytes, 1) end
if char_byte < 0xC0 then
return math.min(max_bytes, 1)
elseif char_byte < 0xE0 then
return math.min(max_bytes, 2)
elseif char_byte < 0xF0 then
return math.min(max_bytes, 3)
elseif char_byte < 0xF8 then
return math.min(max_bytes, 4)
else
return math.min(max_bytes, 1)
end
end
---Creates an iterator for an utf-8 encoded string
---Iterates over utf-8 characters instead of bytes
---@param str string
---@return fun(): integer?, string?
local function utf8_iter(str)
function utf8_iter(str)
local byte_start = 1
return function()
local start = byte_start
@@ -74,6 +80,17 @@ local function utf8_iter(str)
end
end
---Estimating string length based on the number of characters
---@param char string
---@return number
function utf8_length(str)
local str_length = 0
for _, c in utf8_iter(str) do
str_length = str_length + 1
end
return str_length
end
---Extract Unicode code point from utf-8 character at index i in str
---@param str string
---@param i integer
@@ -98,13 +115,19 @@ end
---@param unicode integer
---@return string?
local function unicode_to_utf8(unicode)
if unicode < 0x80 then return string.char(unicode)
if unicode < 0x80 then
return string.char(unicode)
else
local byte_count
if unicode < 0x800 then byte_count = 2
elseif unicode < 0x10000 then byte_count = 3
elseif unicode < 0x110000 then byte_count = 4
else return end -- too big
if unicode < 0x800 then
byte_count = 2
elseif unicode < 0x10000 then
byte_count = 3
elseif unicode < 0x110000 then
byte_count = 4
else
return
end -- too big
local res = {}
local shift = 2 ^ 6
@@ -128,13 +151,13 @@ local function update_osd_resolution(width, height)
if width > 0 and height > 0 then osd_width, osd_height = width, height end
end
mp.observe_property('osd-dimensions', 'native', function (_, dim)
mp.observe_property('osd-dimensions', 'native', function(_, dim)
if dim then update_osd_resolution(dim.w, dim.h) end
end)
local measure_bounds
do
local text_osd = mp.create_osd_overlay("ass-events")
local text_osd = mp.create_osd_overlay('ass-events')
text_osd.compute_bounds, text_osd.hidden = true, true
---@param ass_text string
@@ -252,8 +275,8 @@ do
local unicode = utf8_to_unicode(char, 1)
for _, block in ipairs(zero_width_blocks) do
if unicode >= block[1] and unicode <= block[2] then
char_widths[char] = {0, INFINITY}
return 0, INFINITY
char_widths[char] = {0, math.huge}
return 0, math.huge
end
end
@@ -277,8 +300,11 @@ do
local size = math.min(max_size * 0.9, 50)
char_count = math.min(math.floor(char_count * max_size / size * 0.8), 100)
local enclosing_char, enclosing_width, next_char_count = '|', 0, char_count
if measured_char == enclosing_char then enclosing_char = ''
else enclosing_width = 2 * character_width(enclosing_char, bold) end
if measured_char == enclosing_char then
enclosing_char = ''
else
enclosing_width = 2 * character_width(enclosing_char, bold)
end
local width_ratio, width, px = nil, nil, nil
repeat
char_count = next_char_count
@@ -303,8 +329,8 @@ end
---@return number, integer
local function character_based_width(text, bold)
local max_width = 0
local min_px = INFINITY
for line in tostring(text):gmatch("([^\n]*)\n?") do
local min_px = math.huge
for line in tostring(text):gmatch('([^\n]*)\n?') do
local total_width = 0
for _, char in utf8_iter(line) do
local width, px = character_width(char, bold)
@@ -356,7 +382,7 @@ do
---@type boolean, boolean
local bold, italic = opts.bold or options.font_bold, opts.italic or false
if options.text_width_estimation then
if not config.refine.text_width then
---@type {[string|number]: {[1]: number, [2]: integer}}
local text_width = get_cache_stage(width_cache, bold)
local width_px = text_width[text]
@@ -382,81 +408,108 @@ do
---@type {[string]: string}
local cache = {}
---Get width of formatted timestamp as if all the digits were replaced with 0
function timestamp_zero_rep_clear_cache()
cache = {}
end
---Replace all timestamp digits with 0
---@param timestamp string
---@param opts {size: number; bold?: boolean; italic?: boolean}
---@return number
function timestamp_width(timestamp, opts)
function timestamp_zero_rep(timestamp)
local substitute = cache[#timestamp]
if not substitute then
substitute = timestamp:gsub('%d', '0')
cache[#timestamp] = substitute
end
return text_width(substitute, opts)
return substitute
end
---Get width of formatted timestamp as if all the digits were replaced with 0
---@param timestamp string
---@param opts {size: number; bold?: boolean; italic?: boolean}
---@return number
function timestamp_width(timestamp, opts)
return text_width(timestamp_zero_rep(timestamp), opts)
end
end
---Wrap the text at the closest opportunity to target_line_length
---@param text string
---@param opts {size: number; bold?: boolean; italic?: boolean}
---@param target_line_length number
---@return string
function wrap_text(text, opts, target_line_length)
local target_line_width = target_line_length * width_length_ratio * opts.size
local bold, scale_factor, scale_offset = opts.bold or false, opts_factor_offset(opts)
do
local wrap_at_chars = {' ', ' ', '-', ''}
local remove_when_wrap = {' ', ' '}
local lines = {}
for text_line in text:gmatch("([^\n]*)\n?") do
local line_width = scale_offset
local line_start = 1
local before_end = nil
local before_width = scale_offset
local before_line_start = 0
local before_removed_width = 0
for char_start, char in utf8_iter(text_line) do
local char_end = char_start + #char - 1
local can_wrap = false
for _, c in ipairs(wrap_at_chars) do
if char == c then
can_wrap = true
break
---Wrap the text at the closest opportunity to target_line_length
---@param text string
---@param opts {size: number; bold?: boolean; italic?: boolean}
---@param target_line_length number
---@return string, integer
function wrap_text(text, opts, target_line_length)
local target_line_width = target_line_length * width_length_ratio * opts.size
local bold, scale_factor, scale_offset = opts.bold or false, opts_factor_offset(opts)
local wrap_at_chars, remove_when_wrap = wrap_at_chars, remove_when_wrap
local lines = {}
for _, text_line in ipairs(split(text, '\n')) do
local line_width = scale_offset
local line_start = 1
local before_end = nil
local before_width = scale_offset
local before_line_start = 0
local before_removed_width = 0
for char_start, char in utf8_iter(text_line) do
local char_end = char_start + #char - 1
local char_width = character_width(char, bold) * scale_factor
line_width = line_width + char_width
if (char_end == #text_line) or itable_has(wrap_at_chars, char) then
local remove = itable_has(remove_when_wrap, char)
local line_width_after_remove = line_width - (remove and char_width or 0)
if line_width_after_remove < target_line_width then
before_end = remove and char_start - 1 or char_end
before_width = line_width_after_remove
before_line_start = char_end + 1
before_removed_width = remove and char_width or 0
else
if (target_line_width - before_width) <
(line_width_after_remove - target_line_width) then
lines[#lines + 1] = text_line:sub(line_start, before_end)
line_start = before_line_start
line_width = line_width - before_width - before_removed_width + scale_offset
else
lines[#lines + 1] = text_line:sub(line_start, remove and char_start - 1 or char_end)
line_start = char_end + 1
line_width = scale_offset
end
before_end = line_start
before_width = scale_offset
end
end
end
local char_width = character_width(char, bold) * scale_factor
line_width = line_width + char_width
if can_wrap or (char_end == #text_line) then
local remove = false
for _, c in ipairs(remove_when_wrap) do
if char == c then
remove = true
break
end
end
local line_width_after_remove = line_width - (remove and char_width or 0)
if line_width_after_remove < target_line_width then
before_end = remove and char_start - 1 or char_end
before_width = line_width_after_remove
before_line_start = char_end + 1
before_removed_width = remove and char_width or 0
else
if (target_line_width - before_width) <
(line_width_after_remove - target_line_width) then
lines[#lines + 1] = text_line:sub(line_start, before_end)
line_start = before_line_start
line_width = line_width - before_width - before_removed_width + scale_offset
else
lines[#lines + 1] = text_line:sub(line_start, remove and char_start - 1 or char_end)
line_start = char_end + 1
line_width = scale_offset
end
before_end = line_start
before_width = scale_offset
end
if #text_line >= line_start then
lines[#lines + 1] = text_line:sub(line_start)
elseif text_line == '' then
lines[#lines + 1] = ''
end
end
if #text_line >= line_start then lines[#lines + 1] = text_line:sub(line_start)
elseif text_line == '' then lines[#lines + 1] = '' end
return table.concat(lines, '\n'), #lines
end
end
do
local word_separators = create_set({
' ', ' ', '\t', '-', '', '_', ',', '.', '+', '&', '(', ')', '[', ']', '{', '}', '<', '>', '/', '\\',
'', '', '', '', '', '', '', '', '', '', '', '', '', '',
})
---Get the first character of each word
---@param str string
---@return string[]
function initials(str)
local initials, is_word_start, word_separators = {}, true, word_separators
for _, char in utf8_iter(str) do
if word_separators[char] then
is_word_start = true
elseif is_word_start then
initials[#initials + 1] = char
is_word_start = false
end
end
return initials
end
return table.concat(lines, '\n')
end

View File

@@ -1,105 +1,161 @@
--[[ UI specific utilities that might or might not depend on its state or options ]]
-- Sorting comparator close to (but not exactly) how file explorers sort files.
sort_filenames = (function()
local symbol_order
local default_order
---@alias Point {x: number; y: number}
---@alias Rect {ax: number, ay: number, bx: number, by: number, window_drag?: boolean}
---@alias Circle {point: Point, r: number, window_drag?: boolean}
---@alias Hitbox Rect|Circle
---@alias Shortcut {id: string; key: string; modifiers: string; alt: boolean; ctrl: boolean; shift: boolean}
---@alias ComplexBindingInfo {event: 'down' | 'repeat' | 'up' | 'press'; is_mouse: boolean; canceled: boolean; key_name?: string; key_text?: string;}
if state.platform == 'windows' then
symbol_order = {
['!'] = 1, ['#'] = 2, ['$'] = 3, ['%'] = 4, ['&'] = 5, ['('] = 6, [')'] = 6, [','] = 7,
['.'] = 8, ["'"] = 9, ['-'] = 10, [';'] = 11, ['@'] = 12, ['['] = 13, [']'] = 13, ['^'] = 14,
['_'] = 15, ['`'] = 16, ['{'] = 17, ['}'] = 17, ['~'] = 18, ['+'] = 19, ['='] = 20,
}
default_order = 21
else
symbol_order = {
['`'] = 1, ['^'] = 2, ['~'] = 3, ['='] = 4, ['_'] = 5, ['-'] = 6, [','] = 7, [';'] = 8,
['!'] = 9, ["'"] = 10, ['('] = 11, [')'] = 11, ['['] = 12, [']'] = 12, ['{'] = 13, ['}'] = 14,
['@'] = 15, ['$'] = 16, ['*'] = 17, ['&'] = 18, ['%'] = 19, ['+'] = 20, ['.'] = 22, ['#'] = 23,
}
default_order = 21
--- In place sorting of filenames
---@param filenames string[]
-- String sorting
do
----- winapi start -----
-- in windows system, we can use the sorting function provided by the win32 API
-- see https://learn.microsoft.com/en-us/windows/win32/api/shlwapi/nf-shlwapi-strcmplogicalw
-- this function was taken from https://github.com/mpvnet-player/mpv.net/issues/575#issuecomment-1817413401
local winapi = nil
if state.platform == 'windows' and config.refine.sorting then
-- is_ffi_loaded is false usually means the mpv builds without luajit
local is_ffi_loaded, ffi = pcall(require, 'ffi')
if is_ffi_loaded then
winapi = {
ffi = ffi,
C = ffi.C,
CP_UTF8 = 65001,
shlwapi = ffi.load('shlwapi'),
}
-- ffi code from https://github.com/po5/thumbfast, Mozilla Public License Version 2.0
ffi.cdef [[
int __stdcall MultiByteToWideChar(unsigned int CodePage, unsigned long dwFlags, const char *lpMultiByteStr,
int cbMultiByte, wchar_t *lpWideCharStr, int cchWideChar);
int __stdcall StrCmpLogicalW(wchar_t *psz1, wchar_t *psz2);
]]
winapi.utf8_to_wide = function(utf8_str)
if utf8_str then
local utf16_len = winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, utf8_str, -1, nil, 0)
if utf16_len > 0 then
local utf16_str = winapi.ffi.new('wchar_t[?]', utf16_len)
if winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, utf8_str, -1, utf16_str, utf16_len) > 0 then
return utf16_str
end
end
end
return ''
end
end
end
----- winapi end -----
-- Alphanumeric sorting for humans in Lua
-- alphanum sorting for humans in Lua
-- http://notebook.kulchenko.com/algorithms/alphanumeric-natural-sorting-for-humans-in-lua
local function pad_number(n, d)
return #d > 0 and ("%03d%s%.12f"):format(#n, n, tonumber(d) / (10 ^ #d))
or ("%03d%s"):format(#n, n)
local function padnum(n, d)
return #d > 0 and ('%03d%s%.12f'):format(#n, n, tonumber(d) / (10 ^ #d))
or ('%03d%s'):format(#n, n)
end
--- In place sorting of filenames
---@param filenames string[]
return function(filenames)
local function sort_lua(strings)
local tuples = {}
for i, filename in ipairs(filenames) do
local first_char = filename:sub(1, 1)
local order = symbol_order[first_char] or default_order
local formatted = filename:lower():gsub('0*(%d+)%.?(%d*)', pad_number)
tuples[i] = {order, formatted, filename}
for i, f in ipairs(strings) do
tuples[i] = {f:lower():gsub('0*(%d+)%.?(%d*)', padnum), f}
end
table.sort(tuples, function(a, b)
if a[1] ~= b[1] then return a[1] < b[1] end
return a[2] == b[2] and #b[3] < #a[3] or a[2] < b[2]
return a[1] == b[1] and #b[2] < #a[2] or a[1] < b[1]
end)
for i, tuple in ipairs(tuples) do filenames[i] = tuple[3] end
for i, tuple in ipairs(tuples) do strings[i] = tuple[2] end
return strings
end
end)()
---@param strings string[]
function sort_strings(strings)
if winapi then
table.sort(strings, function(a, b)
return winapi.shlwapi.StrCmpLogicalW(winapi.utf8_to_wide(a), winapi.utf8_to_wide(b)) == -1
end)
else
sort_lua(strings)
end
end
end
-- Creates in-between frames to animate value from `from` to `to` numbers.
---@param from number
---@param to number|fun():number
---@param setter fun(value: number)
---@param factor_or_callback? number|fun()
---@param duration_or_callback? number|fun() Duration in milliseconds or a callback function.
---@param callback? fun() Called either on animation end, or when animation is killed.
function tween(from, to, setter, factor_or_callback, callback)
local factor = factor_or_callback
if type(factor_or_callback) == 'function' then callback = factor_or_callback end
if type(factor) ~= 'number' then factor = 0.3 end
function tween(from, to, setter, duration_or_callback, callback)
local duration = duration_or_callback
if type(duration_or_callback) == 'function' then callback = duration_or_callback end
if type(duration) ~= 'number' then duration = options.animation_duration end
local current, done, timeout = from, false, nil
local get_to = type(to) == 'function' and to or function() return to --[[@as number]] end
local cutoff = math.abs(get_to() - from) * 0.01
local distance = math.abs(get_to() - current)
local cutoff = distance * 0.01
local target_ticks = (math.max(duration, 1) / (state.render_delay * 1000))
local decay = 1 - ((cutoff / distance) ^ (1 / target_ticks))
local function finish()
if not done then
setter(get_to())
done = true
timeout:kill()
if callback then callback() end
request_render()
end
end
local function tick()
local to = get_to()
current = current + ((to - current) * factor)
current = current + ((to - current) * decay)
local is_end = math.abs(to - current) <= cutoff
setter(is_end and to or current)
request_render()
if is_end then finish()
else timeout:resume() end
if is_end then
finish()
else
setter(current)
timeout:resume()
request_render()
end
end
timeout = mp.add_timeout(state.render_delay, tick)
tick()
if cutoff > 0 then tick() else finish() end
return finish
end
---@param point {x: number; y: number}
---@param rect {ax: number; ay: number; bx: number; by: number}
---@param point Point
---@param rect Rect
function get_point_to_rectangle_proximity(point, rect)
local dx = math.max(rect.ax - point.x, 0, point.x - rect.bx)
local dy = math.max(rect.ay - point.y, 0, point.y - rect.by)
return math.sqrt(dx * dx + dy * dy)
end
---@param point_a {x: number; y: number}
---@param point_b {x: number; y: number}
---@param point_a Point
---@param point_b Point
function get_point_to_point_proximity(point_a, point_b)
local dx, dy = point_a.x - point_b.x, point_a.y - point_b.y
return math.sqrt(dx * dx + dy * dy)
end
---@param point Point
---@param hitbox Hitbox
function point_collides_with(point, hitbox)
return (hitbox.r and get_point_to_point_proximity(point, hitbox.point) <= hitbox.r) or
(not hitbox.r and get_point_to_rectangle_proximity(point, hitbox --[[@as Rect]]) == 0)
end
---@param lax number
---@param lay number
---@param lbx number
@@ -110,12 +166,14 @@ end
---@param mby number
function get_line_to_line_intersection(lax, lay, lbx, lby, max, may, mbx, mby)
-- Calculate the direction of the lines
local uA = ((mbx-max)*(lay-may) - (mby-may)*(lax-max)) / ((mby-may)*(lbx-lax) - (mbx-max)*(lby-lay))
local uB = ((lbx-lax)*(lay-may) - (lby-lay)*(lax-max)) / ((mby-may)*(lbx-lax) - (mbx-max)*(lby-lay))
local uA = ((mbx - max) * (lay - may) - (mby - may) * (lax - max)) /
((mby - may) * (lbx - lax) - (mbx - max) * (lby - lay))
local uB = ((lbx - lax) * (lay - may) - (lby - lay) * (lax - max)) /
((mby - may) * (lbx - lax) - (mbx - max) * (lby - lay))
-- If uA and uB are between 0-1, lines are colliding
if uA >= 0 and uA <= 1 and uB >= 0 and uB <= 1 then
return lax + (uA * (lbx-lax)), lay + (uA * (lby-lay))
return lax + (uA * (lbx - lax)), lay + (uA * (lby - lay))
end
return nil, nil
@@ -145,7 +203,7 @@ end
---@param ay number
---@param bx number
---@param by number
---@param rect {ax: number; ay: number; bx: number; by: number}
---@param rect Rect
---@return number|nil
function get_ray_to_rectangle_distance(ax, ay, bx, by, rect)
-- Is inside
@@ -252,16 +310,20 @@ function join_path(p1, p2)
local p1, separator = trim_trailing_separator(p1)
-- Prevents joining drive letters with a redundant separator (`C:\\foo`),
-- as `trim_trailing_separator()` doesn't trim separators from drive letters.
return p1:sub(#p1) == separator and p1 .. p2 or p1 .. separator.. p2
return p1:sub(#p1) == separator and p1 .. p2 or p1 .. separator .. p2
end
-- Check if path is absolute.
---@param path string
---@return boolean
function is_absolute(path)
if path:sub(1, 2) == '\\\\' then return true
elseif state.platform == 'windows' then return path:find('^%a+:') ~= nil
else return path:sub(1, 1) == '/' end
if path:sub(1, 2) == '\\\\' then
return true
elseif state.platform == 'windows' then
return path:find('^%a+:') ~= nil
else
return path:sub(1, 1) == '/'
end
end
-- Ensure path is absolute.
@@ -309,9 +371,13 @@ function normalize_path(path)
path = trim_trailing_separator(path)
--Deduplication of path separators
if is_unc then path = path:gsub('(.\\)\\+', '%1')
elseif state.platform == 'windows' then path = path:gsub('\\\\+', '\\')
else path = path:gsub('//+', '/') end
if is_unc then
path = path:gsub('(.\\)\\+', '%1')
elseif state.platform == 'windows' then
path = path:gsub('\\\\+', '\\')
else
path = path:gsub('//+', '/')
end
return path
end
@@ -319,7 +385,7 @@ end
-- Check if path is a protocol, such as `http://...`.
---@param path string
function is_protocol(path)
return type(path) == 'string' and (path:find('^%a[%a%d-_]+://') ~= nil or path:find('^%a[%a%d-_]+:\\?') ~= nil)
return type(path) == 'string' and (path:find('^%a[%w.+-]-://') ~= nil or path:find('^%a[%w.+-]-:%?') ~= nil)
end
---@param path string
@@ -334,9 +400,40 @@ function has_any_extension(path, extensions)
return false
end
---@return string
function get_default_directory()
return mp.command_native({'expand-path', options.default_directory})
---@param key string
---@param modifiers? string
---@return Shortcut
function create_shortcut(key, modifiers)
key = key:lower()
local id_parts, modifiers_set
if modifiers then
id_parts = split(modifiers:lower(), '+')
table.sort(id_parts, function(a, b) return a < b end)
modifiers_set = create_set(id_parts)
modifiers = table.concat(id_parts, '+')
else
id_parts, modifiers, modifiers_set = {}, '', {}
end
id_parts[#id_parts + 1] = key
return table_assign({id = table.concat(id_parts, '+'), key = key, modifiers = modifiers}, modifiers_set)
end
-- Executes mp command defined as a string or an itable, or does nothing if command is any other value.
-- Returns boolean specifying if command was executed or not.
---@param command string | string[] | nil | any
---@return boolean executed `true` if command was executed.
function execute_command(command)
local command_type = type(command)
if command_type == 'string' then
mp.command(command)
return true
elseif command_type == 'table' and #command > 0 then
mp.command_native(command)
return true
end
return false
end
-- Serializes path into its semantic parts.
@@ -362,28 +459,30 @@ end
-- Reads items in directory and splits it into directories and files tables.
---@param path string
---@param allowed_types? string[] Filter `files` table to contain only files with these extensions.
---@return string[]|nil files
---@return string[]|nil directories
function read_directory(path, allowed_types)
---@param opts? {types?: string[], hidden?: boolean}
---@return string[] files
---@return string[] directories
---@return string|nil error
function read_directory(path, opts)
opts = opts or {}
local items, error = utils.readdir(path, 'all')
if not items then
msg.error('Reading files from "' .. path .. '" failed: ' .. error)
return nil, nil
end
local files, directories = {}, {}
if not items then
return files, directories, 'Reading directory "' .. path .. '" failed. Error: ' .. utils.to_string(error)
end
for _, item in ipairs(items) do
if item ~= '.' and item ~= '..' then
if item ~= '.' and item ~= '..' and (opts.hidden or item:sub(1, 1) ~= '.') then
local info = utils.file_info(join_path(path, item))
if info then
if info.is_file then
if not allowed_types or has_any_extension(item, allowed_types) then
if not opts.types or has_any_extension(item, opts.types) then
files[#files + 1] = item
end
else directories[#directories + 1] = item end
else
directories[#directories + 1] = item
end
end
end
end
@@ -395,18 +494,19 @@ end
-- and index of the current file in the table.
-- Returned table will always contain `file_path`, regardless of `allowed_types`.
---@param file_path string
---@param allowed_types? string[] Filter adjacent file types. Does NOT filter out the `file_path`.
function get_adjacent_files(file_path, allowed_types)
---@param opts? {types?: string[], hidden?: boolean}
function get_adjacent_files(file_path, opts)
opts = opts or {}
local current_meta = serialize_path(file_path)
if not current_meta then return end
local files = read_directory(current_meta.dirname)
if not files then return end
sort_filenames(files)
local files, _dirs, error = read_directory(current_meta.dirname, {hidden = opts.hidden})
if error then msg.error(error) return end
sort_strings(files)
local current_file_index
local paths = {}
for _, file in ipairs(files) do
local is_current_file = current_meta.basename == file
if is_current_file or not allowed_types or has_any_extension(file, allowed_types) then
if is_current_file or not opts.types or has_any_extension(file, opts.types) then
paths[#paths + 1] = join_path(current_meta.dirname, file)
if is_current_file then current_file_index = #paths end
end
@@ -419,14 +519,31 @@ end
-- randomness to determine the next item. Loops around if `loop-playlist` is enabled.
---@param paths table
---@param current_index number
---@param delta number
---@param delta number 1 or -1 for forward or backward
function decide_navigation_in_list(paths, current_index, delta)
if #paths < 2 then return #paths, paths[#paths] end
if #paths < 2 then return end
delta = delta < 0 and -1 or 1
-- Shuffle looks at the played files history trimmed to 80% length of the paths
-- and removes all paths in it from the potential shuffle pool. This guarantees
-- no path repetition until at least 80% of the playlist has been exhausted.
if state.shuffle then
state.shuffle_history = state.shuffle_history or {
pos = #state.history,
paths = itable_slice(state.history),
}
state.shuffle_history.pos = state.shuffle_history.pos + delta
local history_path = state.shuffle_history.paths[state.shuffle_history.pos]
local next_index = history_path and itable_index_of(paths, history_path)
if next_index then
return next_index, history_path
end
if delta < 0 then
state.shuffle_history.pos = state.shuffle_history.pos - delta
else
state.shuffle_history.pos = math.min(state.shuffle_history.pos, #state.shuffle_history.paths + 1)
end
local trimmed_history = itable_slice(state.history, -math.floor(#paths * 0.8))
local shuffle_pool = {}
@@ -438,13 +555,18 @@ function decide_navigation_in_list(paths, current_index, delta)
math.randomseed(os.time())
local next_index = shuffle_pool[math.random(#shuffle_pool)]
return next_index, paths[next_index]
local next_path = paths[next_index]
table.insert(state.shuffle_history.paths, state.shuffle_history.pos, next_path)
return next_index, next_path
end
local new_index = current_index + delta
if mp.get_property_native('loop-playlist') then
if new_index > #paths then new_index = new_index % #paths
elseif new_index < 1 then new_index = #paths - new_index end
if new_index > #paths then
new_index = new_index % #paths
elseif new_index < 1 then
new_index = #paths - new_index
end
elseif new_index < 1 or new_index > #paths then
return
end
@@ -455,10 +577,16 @@ end
---@param delta number
function navigate_directory(delta)
if not state.path or is_protocol(state.path) then return false end
local paths, current_index = get_adjacent_files(state.path, config.types.autoload)
local paths, current_index = get_adjacent_files(state.path, {
types = config.types.autoload,
hidden = options.show_hidden_files,
})
if paths and current_index then
local _, path = decide_navigation_in_list(paths, current_index, delta)
if path then mp.commandv('loadfile', path) return true end
if path then
mp.commandv('loadfile', path)
return true
end
end
return false
end
@@ -469,7 +597,10 @@ function navigate_playlist(delta)
if playlist and #playlist > 1 and pos then
local paths = itable_map(playlist, function(item) return normalize_path(item.filename) end)
local index = decide_navigation_in_list(paths, pos, delta)
if index then mp.commandv('playlist-play-index', index - 1) return true end
if index then
mp.commandv('playlist-play-index', index - 1)
return true
end
end
return false
end
@@ -492,19 +623,19 @@ function delete_file(path)
]]
local escaped_path = string.gsub(path, "'", "''")
escaped_path = string.gsub(escaped_path, "", "")
escaped_path = string.gsub(escaped_path, "%%", "%%%%")
ps_code = string.gsub(ps_code, "__path__", escaped_path)
args = { 'powershell', '-NoProfile', '-Command', ps_code }
escaped_path = string.gsub(escaped_path, '', '')
escaped_path = string.gsub(escaped_path, '%%', '%%%%')
ps_code = string.gsub(ps_code, '__path__', escaped_path)
args = {'powershell', '-NoProfile', '-Command', ps_code}
else
args = { 'cmd', '/C', 'del', path }
args = {'cmd', '/C', 'del', path}
end
else
if options.use_trash then
--On Linux and Macos the app trash-cli/trash must be installed first.
args = { 'trash', path }
args = {'trash', path}
else
args = { 'rm', path }
args = {'rm', path}
end
end
return mp.command_native({
@@ -516,26 +647,60 @@ function delete_file(path)
})
end
function delete_file_navigate(delta)
local path, playlist_pos = state.path, state.playlist_pos
local is_local_file = path and not is_protocol(path)
if navigate_item(delta) then
if state.has_playlist then
mp.commandv('playlist-remove', playlist_pos - 1)
end
else
mp.command('stop')
end
if is_local_file then
if Menu:is_open('open-file') then
Elements:maybe('menu', 'delete_value', path)
end
delete_file(path)
end
end
function serialize_chapter_ranges(normalized_chapters)
local ranges = {}
local simple_ranges = {
{name = 'openings', patterns = {
{
name = 'openings',
patterns = {
'^op ', '^op$', ' op$',
'^opening$', ' opening$'
}, requires_next_chapter = true},
{name = 'intros', patterns = {
'^opening$', ' opening$',
},
requires_next_chapter = true,
},
{
name = 'intros',
patterns = {
'^intro$', ' intro$',
'^avant$', '^prologue$'
}, requires_next_chapter = true},
{name = 'endings', patterns = {
'^avant$', '^prologue$',
},
requires_next_chapter = true,
},
{
name = 'endings',
patterns = {
'^ed ', '^ed$', ' ed$',
'^ending ', '^ending$', ' ending$',
}},
{name = 'outros', patterns = {
},
},
{
name = 'outros',
patterns = {
'^outro$', ' outro$',
'^closing$', '^closing ',
'^preview$', '^pv$',
}},
},
},
}
local sponsor_ranges = {}
@@ -547,7 +712,7 @@ function serialize_chapter_ranges(normalized_chapters)
-- Clone chapters
local chapters = {}
for i, normalized in ipairs(normalized_chapters) do chapters[i] = table_shallow_copy(normalized) end
for i, normalized in ipairs(normalized_chapters) do chapters[i] = table_assign({}, normalized) end
for i, chapter in ipairs(chapters) do
-- Simple ranges
@@ -559,7 +724,7 @@ function serialize_chapter_ranges(normalized_chapters)
if next_chapter or not meta.requires_next_chapter then
ranges[#ranges + 1] = table_assign({
start = chapter.time,
['end'] = next_chapter and next_chapter.time or INFINITY,
['end'] = next_chapter and next_chapter.time or math.huge,
}, config.chapter_ranges[meta.name])
end
end
@@ -575,8 +740,10 @@ function serialize_chapter_ranges(normalized_chapters)
local end_match = end_chapter.lowercase_title:match('segment end *%(' .. id .. '%)')
if end_match then
local range = table_assign({
start_chapter = chapter, end_chapter = end_chapter,
start = chapter.time, ['end'] = end_chapter.time,
start_chapter = chapter,
end_chapter = end_chapter,
start = chapter.time,
['end'] = end_chapter.time,
}, config.chapter_ranges.ads)
ranges[#ranges + 1], sponsor_ranges[#sponsor_ranges + 1] = range, range
end_chapter.is_end_only = true
@@ -588,7 +755,7 @@ function serialize_chapter_ranges(normalized_chapters)
local next_chapter = chapters[i + 1]
ranges[#ranges + 1] = table_assign({
start = chapter.time,
['end'] = next_chapter and next_chapter.time or INFINITY,
['end'] = next_chapter and next_chapter.time or math.huge,
}, config.chapter_ranges.ads)
end
end
@@ -635,24 +802,106 @@ function serialize_chapters(chapters)
local opts = {size = 1, bold = true}
for index, chapter in ipairs(chapters) do
chapter.index = index
chapter.title_wrapped = wrap_text(chapter.title, opts, 25)
chapter.title_wrapped, chapter.title_lines = wrap_text(chapter.title, opts, 25)
chapter.title_wrapped_width = text_width(chapter.title_wrapped, opts)
chapter.title_wrapped = ass_escape(chapter.title_wrapped)
end
return chapters
end
---Find all active key bindings or the active key binding for key
---@param key string|nil
---@return {[string]: table}|table
function find_active_keybindings(key)
local bindings = mp.get_property_native('input-bindings', {})
local active_map = {} -- map: key-name -> bind-info
local active_table = {}
for _, bind in pairs(bindings) do
if bind.owner ~= 'uosc' and bind.priority >= 0 and (not key or bind.key == key) and (
not active_map[bind.key]
or (active_map[bind.key].is_weak and not bind.is_weak)
or (bind.is_weak == active_map[bind.key].is_weak and bind.priority > active_map[bind.key].priority)
)
then
active_table[#active_table + 1] = bind
active_map[bind.key] = bind
end
end
return key and active_map[key] or active_table
end
---@param type 'sub'|'audio'|'video'
---@param path string
function load_track(type, path)
mp.commandv(type .. '-add', path, 'cached')
-- If subtitle track was loaded, assume the user also wants to see it
if type == 'sub' then
mp.commandv('set', 'sub-visibility', 'yes')
end
end
---@return string|nil
function get_clipboard()
local result = mp.command_native({
name = 'subprocess',
capture_stderr = true,
capture_stdout = true,
playback_only = false,
args = {config.ziggy_path, 'get-clipboard'},
})
local function print_error(message)
msg.error('Getting clipboard data failed. Error: ' .. message)
end
if result.status == 0 then
local data = utils.parse_json(result.stdout)
if data and data.payload then
return data.payload
else
print_error(data and (data.error and data.message or 'unknown error') or 'couldn\'t parse json')
end
else
print_error('exit code ' .. result.status .. ': ' .. result.stdout .. result.stderr)
end
end
--[[ RENDERING ]]
function render()
if not display.initialized then return end
state.render_last_time = mp.get_time()
cursor.reset_handlers()
cursor:clear_zones()
-- Click on empty area detection
if setup_click_detection then setup_click_detection() end
-- Actual rendering
local ass = assdraw.ass_new()
-- Idle indicator
if state.is_idle and not Manager.disabled.idle_indicator then
local smaller_side = math.min(display.width, display.height)
local center_x, center_y, icon_size = display.width / 2, display.height / 2, math.max(smaller_side / 4, 56)
ass:icon(center_x, center_y - icon_size / 4, icon_size, 'not_started', {
color = fg, opacity = config.opacity.idle_indicator,
})
ass:txt(center_x, center_y + icon_size / 2, 8, t('Drop files or URLs to play here'), {
size = icon_size / 4, color = fg, opacity = config.opacity.idle_indicator,
})
end
-- Audio indicator
if state.is_audio and not state.has_image and not Manager.disabled.audio_indicator
and not (state.pause and options.pause_indicator == 'static') then
local smaller_side = math.min(display.width, display.height)
ass:icon(display.width / 2, display.height / 2, smaller_side / 4, 'graphic_eq', {
color = fg, opacity = config.opacity.audio_indicator,
})
end
-- Elements
for _, element in Elements:ipairs() do
if element.enabled then
local result = element:maybe('render')
@@ -663,7 +912,7 @@ function render()
end
end
cursor.decide_keybinds()
cursor:decide_keybinds()
-- submit
if osd.res_x == display.width and osd.res_y == display.height and osd.data == ass.text then

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,39 @@
package commands
import (
"flag"
"fmt"
"uosc/bins/src/ziggy/lib"
"github.com/atotto/clipboard"
)
type ClipboardResult struct {
Payload string `json:"payload"`
}
func GetClipboard(_ []string) {
fmt.Print(string(lib.Must(lib.JSONMarshal(ClipboardResult{
Payload: lib.Must(clipboard.ReadAll()),
}))))
}
func SetClipboard(args []string) {
cmd := flag.NewFlagSet("set-clipboard", flag.ExitOnError)
lib.Check(cmd.Parse(args))
values := cmd.Args()
value := ""
if len(values) > 0 {
value = values[0]
}
lib.Check(cmd.Parse(args))
lib.Check(clipboard.WriteAll(value))
fmt.Print(string(lib.Must(lib.JSONMarshal(ClipboardResult{
Payload: value,
}))))
}

View File

@@ -0,0 +1,175 @@
package commands
import (
"bytes"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"slices"
"strings"
"uosc/bins/src/ziggy/lib"
)
const OPEN_SUBTITLES_API_URL = "https://api.opensubtitles.com/api/v1"
type DownloadRequestData struct {
FileId int `json:"file_id"`
}
type DownloadResponseData struct {
Link string `json:"link"`
FileName string `json:"file_name"`
Requests int `json:"requests"`
Remaining int `json:"remaining"`
Message string `json:"message"`
ResetTime string `json:"reset_time"`
ResetTimeUTC string `json:"reset_time_utc"`
}
type DownloadData struct {
File string `json:"file"`
Remaining int `json:"remaining"`
Total int `json:"total"`
ResetTime string `json:"reset_time"`
}
func SearchSubtitles(args []string) {
cmd := flag.NewFlagSet("search-subtitles", flag.ExitOnError)
argApiKey := cmd.String("api-key", "", "Open Subtitles consumer API key.")
argAgent := cmd.String("agent", "", "User-Agent header. Format: appname v1.0")
argLanguages := cmd.String("languages", "", "What languages to search for.")
argHash := cmd.String("hash", "", "What file to hash and add to search query.")
argQuery := cmd.String("query", "", "String query to use.")
argPage := cmd.Int("page", 1, "Results page, starting at 1.")
lib.Check(cmd.Parse(args))
// Validation
if len(*argApiKey) == 0 {
lib.Check(errors.New("--api-key is required"))
}
if len(*argAgent) == 0 {
lib.Check(errors.New("--agent is required"))
}
if len(*argHash) == 0 && len(*argQuery) == 0 {
lib.Check(errors.New("at least one of --query or --hash is required"))
}
if len(*argLanguages) == 0 {
lib.Check(errors.New("--languages is required"))
}
// "Send request parameters sorted, and send all queries in lowercase."
params := []string{}
languageDelimiterRE := regexp.MustCompile(" *, *")
languages := languageDelimiterRE.Split(*argLanguages, -1)
slices.Sort(languages)
params = append(params, "languages="+escapeParam(strings.Join(languages, ",")))
if len(*argHash) > 0 {
hash, err := lib.OSDBHashFile(*argHash)
if err == nil {
params = append(params, "moviehash="+escapeParam(hash))
} else if len(*argQuery) == 0 {
lib.Check(fmt.Errorf("couldn't hash the file (%w) and query is empty", err))
}
}
params = append(params, "page="+escapeParam(fmt.Sprint(*argPage)))
if len(*argQuery) > 0 {
params = append(params, "query="+escapeParam(*argQuery))
}
client := http.Client{}
req := lib.Must(http.NewRequest("GET", OPEN_SUBTITLES_API_URL+"/subtitles?"+strings.Join(params, "&"), nil))
req.Header = http.Header{
"Api-Key": {*argApiKey},
"User-Agent": {*argAgent},
}
resp := lib.Must(client.Do(req))
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
fmt.Print(string(lib.Must(io.ReadAll(resp.Body))))
} else {
lib.Check(errors.New(resp.Status))
}
}
func DownloadSubtitles(args []string) {
cmd := flag.NewFlagSet("download-subtitles", flag.ExitOnError)
argApiKey := cmd.String("api-key", "", "Open Subtitles consumer API key.")
argAgent := cmd.String("agent", "", "User-Agent header. Format: appname v1.0")
argFileID := cmd.Int("file-id", 0, "Subtitle file ID to download.")
argDestination := cmd.String("destination", "", "Destination directory.")
lib.Check(cmd.Parse(args))
// Validation
if len(*argApiKey) == 0 {
lib.Check(errors.New("--api-key is required"))
}
if len(*argAgent) == 0 {
lib.Check(errors.New("--agent is required"))
}
if *argFileID == 0 {
lib.Check(errors.New("--file-id is required"))
}
if len(*argDestination) == 0 {
lib.Check(errors.New("--destination is required"))
}
// Create the directory if it doesn't exist
if _, err := os.Stat(*argDestination); os.IsNotExist(err) {
os.MkdirAll(*argDestination, 0755)
}
data := bytes.NewBuffer(lib.Must(lib.JSONMarshal(DownloadRequestData{FileId: *argFileID})))
client := http.Client{}
req := lib.Must(http.NewRequest("POST", OPEN_SUBTITLES_API_URL+"/download", data))
req.Header = http.Header{
"Accept": {"application/json"},
"Api-Key": {*argApiKey},
"Content-Type": {"application/json"},
"User-Agent": {*argAgent},
}
resp := lib.Must(client.Do(req))
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
lib.Check(errors.New(resp.Status))
}
var downloadData DownloadResponseData
lib.Check(json.Unmarshal(lib.Must(io.ReadAll(resp.Body)), &downloadData))
filePath := filepath.Join(*argDestination, downloadData.FileName)
outFile := lib.Must(os.Create(filePath))
defer outFile.Close()
response := lib.Must(http.Get(downloadData.Link))
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
lib.Check(fmt.Errorf("downloading failed: %s", response.Status))
}
lib.Must(io.Copy(outFile, response.Body))
fmt.Print(string(lib.Must(lib.JSONMarshal(DownloadData{
File: filePath,
Remaining: downloadData.Remaining,
Total: downloadData.Remaining + downloadData.Requests,
ResetTime: downloadData.ResetTime,
}))))
}
// Escape and lowercase (open subtitles requirement) a URL parameter
func escapeParam(str string) string {
return url.QueryEscape(strings.ToLower(str))
}

98
src/ziggy/lib/utils.go Normal file
View File

@@ -0,0 +1,98 @@
package lib
import (
"bytes"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"os"
)
type ErrorData struct {
Error bool `json:"error"`
Message string `json:"message"`
}
func Check(err error) {
if err != nil {
res := ErrorData{Error: true, Message: err.Error()}
json, err := json.Marshal(res)
if err != nil {
panic(err)
}
fmt.Print(string(json))
os.Exit(0)
}
}
func Must[T any](t T, err error) T {
Check(err)
return t
}
const OSDBChunkSize = 65536 // 64k
// Generate an OSDB hash for a file.
func OSDBHashFile(filePath string) (hash string, err error) {
file, err := os.Open(filePath)
if err != nil {
return "", errors.New("couldn't open file for hashing")
}
fi, err := file.Stat()
if err != nil {
return "", errors.New("couldn't stat file for hashing")
}
if fi.Size() < OSDBChunkSize {
return "", errors.New("file is too small to generate a valid OSDB hash")
}
// Read head and tail blocks
buf := make([]byte, OSDBChunkSize*2)
err = readChunk(file, 0, buf[:OSDBChunkSize])
if err != nil {
return
}
err = readChunk(file, fi.Size()-OSDBChunkSize, buf[OSDBChunkSize:])
if err != nil {
return
}
// Convert to uint64, and sum
var nums [(OSDBChunkSize * 2) / 8]uint64
reader := bytes.NewReader(buf)
err = binary.Read(reader, binary.LittleEndian, &nums)
if err != nil {
return "", err
}
var hashUint uint64
for _, num := range nums {
hashUint += num
}
hashUint = hashUint + uint64(fi.Size())
return fmt.Sprintf("%016x", hashUint), nil
}
// Read a chunk of a file at `offset` so as to fill `buf`.
func readChunk(file *os.File, offset int64, buf []byte) (err error) {
n, err := file.ReadAt(buf, offset)
if err != nil {
return err
}
if n != OSDBChunkSize {
return fmt.Errorf("invalid read %v", n)
}
return
}
// Because the default `json.Marshal` HTML escapes `&,<,>` characters and it can't be turned off...
func JSONMarshal(t interface{}) ([]byte, error) {
buffer := &bytes.Buffer{}
encoder := json.NewEncoder(buffer)
encoder.SetEscapeHTML(false)
err := encoder.Encode(t)
return buffer.Bytes(), err
}

33
src/ziggy/ziggy.go Normal file
View File

@@ -0,0 +1,33 @@
package main
import (
"errors"
"os"
"uosc/bins/src/ziggy/commands"
)
func main() {
command := "help"
args := os.Args[2:]
if len(os.Args) > 1 {
command = os.Args[1]
}
switch command {
case "search-subtitles":
commands.SearchSubtitles(args)
case "download-subtitles":
commands.DownloadSubtitles(args)
case "get-clipboard":
commands.GetClipboard(args)
case "set-clipboard":
commands.SetClipboard(args)
default:
panic(errors.New("command required"))
}
}

View File

@@ -1,5 +1,3 @@
local example_chapters = {
openings = {
yes = {
@@ -7,8 +5,8 @@ local example_chapters = {
'Opening',
},
no = {
'Opening the box'
}
'Opening the box',
},
},
intros = {
yes = {
@@ -18,7 +16,7 @@ local example_chapters = {
'Prologue',
},
no = {
}
},
},
endings = {
yes = {
@@ -28,7 +26,7 @@ local example_chapters = {
no = {
'end of the thread',
'trending',
}
},
},
outros = {
yes = {
@@ -38,28 +36,42 @@ local example_chapters = {
'PV',
},
no = {
}
}
},
},
}
local simple_ranges = {
{name = 'openings', patterns = {
{
name = 'openings',
patterns = {
'^op ', '^op$', ' op$',
'^opening$', ' opening$'
}, requires_next_chapter = true},
{name = 'intros', patterns = {
'^opening$', ' opening$',
},
requires_next_chapter = true
},
{
name = 'intros',
patterns = {
'^intro$', ' intro$',
'^avant$', '^prologue$'
}, requires_next_chapter = true},
{name = 'endings', patterns = {
'^avant$', '^prologue$',
},
requires_next_chapter = true
},
{
name = 'endings',
patterns = {
'^ed ', '^ed$', ' ed$',
'^ending ', '^ending$', ' ending$',
}},
{name = 'outros', patterns = {
}
},
{
name = 'outros',
patterns = {
'^outro$', ' outro$',
'^closing$', '^closing ',
'^preview$', '^pv$',
}},
}
},
}
local function find_any(s, patterns)

81
tools/build Executable file
View File

@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# Script to build one of uosc binaries.
# Requirements: go, upx (if compressing)
# Usage: tools/build <name> [-c]
# <name> can be: tools, ziggy
# -c enables binary compression with upx (only needed for builds being released)
abort() {
echo "Error: $1"
exit 1
}
if [ ! -d "$PWD/src" ]; then
abort "'src' directory not found. Make sure this script is run in uosc's repository root as current working directory."
fi
if [ "$1" = "tools" ]; then
export GOARCH="amd64"
src="./src/tools/tools.go"
out_dir="./tools"
echo "Building for Windows..."
export GOOS="windows"
go build -ldflags "-s -w" -o "$out_dir/tools.exe" $src
echo "Building for Linux..."
export GOOS="linux"
go build -ldflags "-s -w" -o "$out_dir/tools-linux" $src
echo "Building for MacOS..."
export GOOS="darwin"
go build -ldflags "-s -w" -o "$out_dir/tools-darwin" $src
if [ "$2" = "-c" ]; then
echo "Compressing binaries..."
upx --brute "$out_dir/tools.exe"
upx --brute "$out_dir/tools-linux"
upx --brute "$out_dir/tools-darwin"
fi
unset GOARCH
unset GOOS
elif [ "$1" = "ziggy" ]; then
export GOARCH="amd64"
src="./src/ziggy/ziggy.go"
out_dir="./src/uosc/bin"
if [ ! -d $out_dir ]; then
mkdir -pv $out_dir
fi
echo "Building for Windows..."
export GOOS="windows"
go build -ldflags "-s -w" -o "$out_dir/ziggy-windows.exe" $src
echo "Building for Linux..."
export GOOS="linux"
go build -ldflags "-s -w" -o "$out_dir/ziggy-linux" $src
echo "Building for MacOS..."
export GOOS="darwin"
go build -ldflags "-s -w" -o "$out_dir/ziggy-darwin" $src
if [ "$2" = "-c" ]; then
echo "Compressing binaries..."
upx "$out_dir/ziggy-windows.exe"
upx "$out_dir/ziggy-linux"
upx "$out_dir/ziggy-darwin"
fi
unset GOARCH
unset GOOS
else
echo "Tool to build one of uosc binaries. Requires go to be installed and in path."
echo "Requirements: go, upx (if compressing)"
echo "Usage: tools/build <name> [-c]"
echo "<name> can be: tools, ziggy"
echo "-c enables binary compression (requires upx)"
fi

81
tools/build.ps1 Normal file
View File

@@ -0,0 +1,81 @@
# Script to build one of uosc binaries.
# Requirements: go, upx (if compressing)
# Usage: tools/build <name> [-c]
# <name> can be: tools, ziggy
# -c enables binary compression with upx (only needed for builds being released)
Function Abort($Message) {
Write-Output "Error: $Message"
Write-Output "Aborting!"
Exit 1
}
if (!(Test-Path -Path "$PWD/src" -PathType Container)) {
Abort("'src' directory not found. Make sure this script is run in uosc's repository root as current working directory.")
}
if ($args[0] -eq "tools") {
$env:GOARCH = "amd64"
$Src = "./src/tools/tools.go"
$OutDir = "./tools"
Write-Output "Building for Windows..."
$env:GOOS = "windows"
go build -ldflags "-s -w" -o "$OutDir/tools.exe" $Src
Write-Output "Building for Linux..."
$env:GOOS = "linux"
go build -ldflags "-s -w" -o "$OutDir/tools-linux" $Src
Write-Output "Building for MacOS..."
$env:GOOS = "darwin"
go build -ldflags "-s -w" -o "$OutDir/tools-darwin" $Src
if ($args[1] -eq "-c") {
Write-Output "Compressing binaries..."
upx --brute "$OutDir/tools.exe"
upx --brute "$OutDir/tools-linux"
upx --brute "$OutDir/tools-darwin"
}
Remove-Item Env:\GOOS
Remove-Item Env:\GOARCH
}
elseif ($args[0] -eq "ziggy") {
$env:GOARCH = "amd64"
$Src = "./src/ziggy/ziggy.go"
$OutDir = "./src/uosc/bin"
if (!(Test-Path $OutDir)) {
New-Item -ItemType Directory -Force -Path $OutDir > $null
}
Write-Output "Building for Windows..."
$env:GOOS = "windows"
go build -ldflags "-s -w" -o "$OutDir/ziggy-windows.exe" $Src
Write-Output "Building for Linux..."
$env:GOOS = "linux"
go build -ldflags "-s -w" -o "$OutDir/ziggy-linux" $Src
Write-Output "Building for MacOS..."
$env:GOOS = "darwin"
go build -ldflags "-s -w" -o "$OutDir/ziggy-darwin" $Src
if ($args[1] -eq "-c") {
Write-Output "Compressing binaries..."
upx "$OutDir/ziggy-windows.exe"
upx "$OutDir/ziggy-linux"
upx "$OutDir/ziggy-darwin"
}
Remove-Item Env:\GOOS
Remove-Item Env:\GOARCH
}
else {
Write-Output "Tool to build one of uosc binaries."
Write-Output "Requirements: go, upx (if compressing)"
Write-Output "Usage: tools/build <name> [-c]"
Write-Output "<name> can be: tools, ziggy"
Write-Output "-c enables binary compression (requires upx)"
}

7
tools/intl Executable file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
if [ "$(uname)" == "Darwin" ]; then
"$SCRIPT_DIR/tools-darwin" intl $*
else
"$SCRIPT_DIR/tools-linux" intl $*
fi

1
tools/intl.ps1 Normal file
View File

@@ -0,0 +1 @@
& "$PSScriptRoot/tools.exe" intl $args

7
tools/package Executable file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
if [ "$(uname)" == "Darwin" ]; then
"$SCRIPT_DIR/tools-darwin" package $*
else
"$SCRIPT_DIR/tools-linux" package $*
fi

1
tools/package.ps1 Normal file
View File

@@ -0,0 +1 @@
& "$PSScriptRoot/tools.exe" package $args

BIN
tools/tools-darwin Executable file

Binary file not shown.

BIN
tools/tools-linux Executable file

Binary file not shown.

BIN
tools/tools.exe Normal file

Binary file not shown.