kew-2.8.2/000077500000000000000000000000001467402032100123105ustar00rootroot00000000000000kew-2.8.2/.editorconfig000066400000000000000000000003461467402032100147700ustar00rootroot00000000000000# EditorConfig is awesome: https://EditorConfig.org # top-most EditorConfig file root = true [*] indent_style = space indent_size = 8 end_of_line = lf charset = utf-8 trim_trailing_whitespace = false insert_final_newline = falsekew-2.8.2/.github/000077500000000000000000000000001467402032100136505ustar00rootroot00000000000000kew-2.8.2/.github/FUNDING.yml000066400000000000000000000000001467402032100154530ustar00rootroot00000000000000kew-2.8.2/.github/workflows/000077500000000000000000000000001467402032100157055ustar00rootroot00000000000000kew-2.8.2/.github/workflows/appimage_alpine.yml000066400000000000000000000056361467402032100215550ustar00rootroot00000000000000name: Alpine-appimage on: release: types: [created] # Triggers on release created workflow_dispatch: jobs: build-and-create-appimage: runs-on: ubuntu-latest container: image: alpine:latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Install build essentials and dependencies run: | apk update apk add --no-cache \ build-base \ ffmpeg-dev \ fftw-dev \ freeimage-dev \ chafa-dev \ opus-dev \ opusfile-dev \ libvorbis-dev \ libnotify-dev \ glib-dev \ wget git desktop-file-utils \ squashfs-tools \ patchelf \ musl \ musl-dev \ gcompat - name: Build code with static linking run: | # export CC=musl-gcc # export LDFLAGS="-static -Wl,-z,relro,-lz" make - name: Prepare AppDir run: | mkdir -p appdir/usr/bin chmod +x ./kew mv ./kew appdir/usr/bin/ mkdir -p appdir/usr/lib - name: Download uploadtool run: | wget -q https://github.com/probonopd/uploadtool/raw/master/upload.sh chmod +x upload.sh mv upload.sh /usr/local/bin/uploadtool - name: Download and prepare appimagetool run: | wget -O appimagetool-x86_64.AppImage -c https://github.com/$(wget -q https://github.com/probonopd/go-appimage/releases/expanded_assets/continuous -O - | grep "appimagetool-.*-x86_64.AppImage" | head -n 1 | cut -d '"' -f 2) if [ ! -f appimagetool-*.AppImage ]; then echo "appimagetool download failed"; exit 1; fi chmod +x appimagetool-x86_64.AppImage - name: Use appimagetool with --appimage-extract-and-run run: | ./appimagetool-x86_64.AppImage --appimage-extract-and-run deploy appdir/usr/share/applications/kew.desktop - name: Remove unnecessary libraries run: | rm appdir/usr/lib/libavcodec* rm appdir/usr/lib/libavformat* rm appdir/usr/lib/libavutil* rm appdir/usr/lib/libswres* - name: Create AppImage run: | mkdir -p output APPIMAGE_EXTRACT_AND_RUN=1 \ ARCH=$(uname -m) \ VERSION=$(./appdir/usr/bin/kew --version | awk -F": " 'FNR==6 {printf $NF}') \ ./appimagetool-*.AppImage ./appdir - name: Move and Rename kew AppImage run: | mv kew*.AppImage output/kew chmod +x output/kew - name: Release uses: marvinpinto/action-automatic-releases@latest with: title: kew appImage (musl systems) automatic_release_tag: stable-musl prerelease: false draft: true files: | output/kew repo_token: ${{ secrets.GITHUB_TOKEN }} kew-2.8.2/.github/workflows/ci.yml000066400000000000000000000011231467402032100170200ustar00rootroot00000000000000name: Build Check on: pull_request: push: branches: - main jobs: ubuntu-build-check: name: Ubuntu Build Check runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Install build essentials run: sudo apt-get update && sudo apt-get install -y build-essential - name: Install dependencies run: sudo apt install -y ffmpeg libfftw3-dev libopus-dev libopusfile-dev libvorbis-dev libchafa-dev libfreeimage-dev libavformat-dev libstb-dev libnotify-dev - name: Build code run: make kew-2.8.2/.gitignore000066400000000000000000000004671467402032100143070ustar00rootroot00000000000000# Prerequisites *.d # Compiled Object files *.slo *.lo *.o *.obj # Precompiled Headers *.gch *.pch # Compiled Dynamic libraries *.so *.dylib *.dll # Fortran module files *.mod *.smod # Compiled Static libraries *.lai *.la *.a *.lib # Executables *.exe *.out *.app kew .vscode/ error.log valgrind-out.txt kew-2.8.2/CONTRIBUTING.md000066400000000000000000000026051467402032100145440ustar00rootroot00000000000000# Welcome to kew music contributing guide Thanks for stopping by, here are some points about contributing to kew. ### Goal of the project The goal of kew is to provide a quick and easy way for people to listen to music with the absolute minimum of inconvenience. It's a small app, limited in scope and it shouldn't be everything to all people. It should continue to be a very light weight app. For instance, it's not imagined as a software for dj'ing or as a busy music file manager with all the features. We want to keep the codebase easy to manage and free of bloat, so might reject a feature out of that reason only. ### Bugs Please report any bugs directly on github, with as much relevant detail as possible. If there's a crash or stability issue, the audio file details are interesting, but also the details of the previous and next file on the playlist. You can extract these details by running: ffprobe -i AUDIO FILE -show_streams -select_streams a:0 -v quiet -print_format json ### Pull Requests - Please contact me (kew-music-player@proton.me) before doing a big change, or risk the whole thing getting rejected. - Try to keep commits fairly small so that they are easy to review. - If you can, use https://editorconfig.org/. There is a file with settings for it: .editorconfig. - If you're fixing a particular bug in the issue list, please explicitly say "Fixes #" in your description". kew-2.8.2/LICENSE000066400000000000000000000432541467402032100133250ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 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. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) 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 this service 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 make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. 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. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. 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 Program or any portion of it, thus forming a work based on the Program, 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) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, 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 Program, 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 Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) 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; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, 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 executable. However, as a special exception, the source code 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. If distribution of executable or 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 counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program 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. 5. 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 Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program 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 to this License. 7. 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 Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program 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 Program. 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. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program 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. 9. The Free Software Foundation may publish revised and/or new versions of the 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 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 Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, 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 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. 12. 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 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. 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 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. Copyright (C) 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 2 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, 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. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision 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, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This 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. kew-2.8.2/Makefile000066400000000000000000000046701467402032100137570ustar00rootroot00000000000000CC ?= gcc PKG_CONFIG ?= pkg-config # Default USE_LIBNOTIFY to auto-detect if not set by user ifeq ($(origin USE_LIBNOTIFY), undefined) ifneq ($(shell $(PKG_CONFIG) --exists libnotify && echo yes),yes) USE_LIBNOTIFY = 0 else USE_LIBNOTIFY = 1 endif endif CFLAGS = -I/usr/include -I/usr/include/chafa -I/usr/lib/chafa/include -I/usr/include/ogg -I/usr/include/opus -I/usr/include/stb -Iinclude/imgtotxt/ext -Iinclude/imgtotxt -I/usr/include/ffmpeg -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include -Iinclude/miniaudio -I/usr/include/gdk-pixbuf-2.0 -O1 $(shell $(PKG_CONFIG) --cflags libavcodec libavutil libavformat libswresample gio-2.0 chafa fftw3f opus opusfile vorbis glib-2.0) CFLAGS += -fstack-protector-strong -Wformat -Werror=format-security -fPIE -fstack-protector -fstack-protector-strong -D_FORTIFY_SOURCE=2 CFLAGS += -Wall -Wextra -Wpointer-arith LIBS = -L/usr/lib -lfreeimage -lpthread -lrt -pthread -lm -lglib-2.0 $(shell $(PKG_CONFIG) --libs libavcodec libavutil libavformat libswresample gio-2.0 chafa fftw3f opus opusfile vorbis vorbisfile glib-2.0) LDFLAGS = -pie -Wl,-z,relro,-lz # Conditionally add libnotify if USE_LIBNOTIFY is enabled ifeq ($(USE_LIBNOTIFY), 1) CFLAGS += $(shell $(PKG_CONFIG) --cflags libnotify) LIBS += -lnotify DEFINES += -DUSE_LIBNOTIFY endif ifeq ($(CC), gcc) LIBS += -latomic endif OBJDIR = src/obj PREFIX = /usr SRCS = src/common_ui.c src/sound.c src/directorytree.c src/soundcommon.c src/search_ui.c src/playlist_ui.c src/player.c src/soundbuiltin.c src/mpris.c src/playerops.c src/utils.c src/file.c src/chafafunc.c src/cache.c src/songloader.c src/playlist.c src/term.c src/settings.c src/visuals.c src/kew.c OBJS = $(SRCS:src/%.c=$(OBJDIR)/%.o) MAN_PAGE = kew.1 MAN_DIR ?= $(PREFIX)/share/man all: kew $(OBJDIR)/%.o: src/%.c Makefile | $(OBJDIR) $(CC) $(CFLAGS) $(DEFINES) -c -o $@ $< $(OBJDIR)/write_ascii.o: include/imgtotxt/write_ascii.c Makefile | $(OBJDIR) $(CC) $(CFLAGS) $(DEFINES) -c -o $@ $< $(OBJDIR): mkdir -p $(OBJDIR) kew: $(OBJDIR)/write_ascii.o $(OBJS) Makefile $(CC) -o kew $(OBJDIR)/write_ascii.o $(OBJS) $(LIBS) $(LDFLAGS) .PHONY: install install: all mkdir -p $(DESTDIR)$(MAN_DIR)/man1 mkdir -p $(DESTDIR)$(PREFIX)/bin install -m 0755 kew $(DESTDIR)$(PREFIX)/bin/kew install -m 0644 docs/kew.1 $(DESTDIR)$(MAN_DIR)/man1/kew.1 .PHONY: uninstall uninstall: rm -f $(DESTDIR)$(PREFIX)/bin/kew rm -f $(DESTDIR)$(MAN_DIR)/man1/kew.1 .PHONY: clean clean: rm -rf $(OBJDIR) kew kew-2.8.2/README.md000066400000000000000000000175541467402032100136030ustar00rootroot00000000000000 # kew [![GitHub license](https://img.shields.io/github/license/ravachol/kew?color=333333&style=for-the-badge)](https://github.com/ravachol/kew/blob/master/LICENSE) Listen to music in the terminal. ![Example screenshot](images/kew-screenshot.png) *Example screenshot running in Konsole: [Jenova 7: Lost Sci-Fi Movie Themes](https://jenova7.bandcamp.com/album/lost-sci-fi-movie-themes).* \ kew (/kjuː/) is a command-line music player for Linux. ## Features * Search a music library with partial titles. * Creates a playlist based on a matched directory. * Control the player with previous, next and pause. * Edit the playlist by adding and removing songs. * Supports gapless playback (between files of the same format and type). * Supports MP3, FLAC, MPEG-4 (AAC, M4A), OPUS, OGG and WAV audio. * Private, no data is collected by kew. ## Installing Packaging status It's advised, if possible, to install from a package or from a release here on github and not from https://github.com/ravachol/kew.git or the install script, because the main branch can and will be unstable sometimes. ### Installing in Debian, Ubuntu It's available from Ubuntu 24.04, Debian 13. ```bash $ apt install kew ``` ### Installing via AUR On Arch Linux, and Arch-based distributions, kew can be found in the AUR. Install with pamac or an AUR helper like yay: ```bash $ yay kew-git ``` Or ```bash $ yay kew ``` ### Installing via Brew For [Homebrew](https://brew.sh/) user, you can install [kew](https://formulae.brew.sh/formula/kew) with: ```bash $ brew install kew ``` ### Installing with quick install script To quickly install kew, just copy and paste this to your terminal (if you have curl installed): ```bash sudo bash -c "curl https://raw.githubusercontent.com/ravachol/kew/main/install.sh | bash" ``` Please note that this script might do a system update before installing kew. ### Standalone AppImage for musl systems If you are running a musl-based system, for instance Alpine Linux, you can download a standalone appImage of kew: https://github.com/ravachol/kew/releases/tag/stable-musl ### Building the project manually kew dependencies are: * FFmpeg * FFTW * Chafa * FreeImage * libopus * opusfile * libvorbis * pkg-config * glib2.0 and AVFormat. These should be installed with the others, if not install them. * libnotify (optional) Install FFmpeg, FFTW, Chafa and FreeImage using your distro's package manager. For instance: ```bash apt install ffmpeg libfftw3-dev libopus-dev libopusfile-dev libvorbis-dev git gcc make libchafa-dev libfreeimage-dev libavformat-dev libglib2.0-dev libnotify-dev ``` Or: ```bash pacman -Syu ffmpeg fftw git gcc make chafa freeimage glib2 opus opusfile libvorbis libnotify ``` Or (for Fedora for instance): ```bash dnf install -y pkg-config ffmpeg-free-devel fftw-devel opus-devel opusfile-devel libvorbis-devel git gcc make chafa-devel freeimage-devel libavformat-free-devel libnotify-devel libatomic ``` Notice that for some packages not only the library needs to be installed, but also development packages, for instance libopus-dev or opus-devel. Then run this (either git clone or unzip a release zip into a folder of your choice): ```bash git clone https://github.com/ravachol/kew.git ``` ```bash cd kew ``` ```bash make ``` ```bash sudo make install ``` A sixel (or equivalent) capable terminal is recommended, like Konsole or kitty, to display images properly. For a complete list of capable terminals, see this page: [Sixels in Terminal](https://www.arewesixelyet.com/). #### LibNotify is (should be) optional By default, the build system will automatically detect if `libnotify` is available and include it and enable notifications if found. ### Uninstalling If you installed kew manually, simply run: ```bash sudo make uninstall ``` ## Usage Run kew. It will first help you set the path to your music folder, then show you that folder's contents. kew can also be told to play a certain music from the command line. It automatically creates a playlist based on a partial name of a track or directory: ```bash kew cure great ``` This command plays all songs from "The Cure Greatest Hits" directory, provided it's in your music library. kew returns the first directory or file whose name matches the string you provide. It works best when your music library is organized in this way: artist folder->album folder(s)->track(s). #### Some Examples: ``` kew (starting kew with no arguments opens the library view where you can choose what to play) kew all (plays all songs, up to 20 000, in your library, shuffled) kew albums (plays all albums, up to 2000, randomly one after the other) kew moonlight son (finds and plays moonlight sonata) kew moon (finds and plays moonlight sonata) kew beet (finds and plays all music files under "beethoven" directory) kew dir (sometimes it's necessary to specify it's a directory you want) kew song (or a song) kew list (or a playlist) kew shuffle (shuffles the playlist) kew artistA:artistB:artistC (plays all three artists, shuffled) kew --help, -? or -h kew --version or -v kew --nocover kew --noui (completely hides the UI) kew -q , --quitonstop (exits after finishing playing the playlist) kew -e , --exact (specifies you want an exact (but not case sensitive) match, of for instance an album) kew . loads kew.m3u kew path "/home/joe/Musik/" (changes the path) ``` Put single-quotes inside quotes "guns n' roses" #### Key Bindings * Use + (or =), - keys to adjust the volume. * Use , or h, l keys to switch tracks. * Space, p to toggle pause. * F2 to show/hide the playlist and information about kew. * F3 to show/hide the library. * F4 to show/hide the track view. * F5 to search. * F6 to show/hide key bindings. * u to update the library. * v to toggle the spectrum visualizer. * i to switch between using your regular color scheme or colors derived from the track cover. * b to toggle album covers drawn in ascii or as a normal image. * r to repeat the current song. * s to shuffle the playlist. * a to seek back. * d to seek forward. * x to save the currently loaded playlist to a m3u file in your music folder. * gg go to first song. * number +G, g or Enter, go to specific song number in the playlist. * g go to last song. * . to add current song to kew.m3u (run with "kew ."). * Esc to quit. ## Configuration kew will create a config file, kewrc, in a kew folder in your default config directory for instance ~/.config/kew. There you can change key bindings, number of bars in the visualizer and whether to use the album cover for color, or your regular color scheme. You can also change the default color of the app here. To edit this file please make sure you quit kew first. ## Nerd Fonts kew looks better with Nerd Fonts: https://www.nerdfonts.com/. ## License Licensed under GPL. [See LICENSE for more information](https://github.com/ravachol/kew/blob/main/LICENSE). ## Attributions kew makes use of the following great open source projects: Chafa by Petter Jansson - https://hpjansson.org/chafa/ FFmpeg by FFmpeg team - https://ffmpeg.org/ FFTW by Matteo Frigo and Steven G. Johnson - https://www.fftw.org/ Libopus by Opus - https://opus-codec.org/ Libvorbis by Xiph.org - https://xiph.org/ Miniaudio by David Reid - https://github.com/mackron/miniaudio Img_To_Txt by Danny Burrows - https://github.com/danny-burrows/img_to_txt Comments? Suggestions? Send mail to kew-music-player@proton.me. ![](images/alien_astronauts-kew.png) kew-2.8.2/SECURITY.md000066400000000000000000000013161467402032100141020ustar00rootroot00000000000000## Reporting a Bug If you find a security related issue, please contact us at kew-music-player@proton.me. When a fix is published, you will receive credit under your real name or bug tracker handle in GitHub. If you prefer to remain anonymous or pseudonymous, you should mention this in your e-mail. ## Disclosure Policy The maintainer will coordinate the fix and release process, involving the following steps: * Confirm the problem and determine the affected versions. * Audit code to find any potential similar problems. * Prepare fixes for all releases still under maintenance. These fixes will be released as fast as possible. You may be asked to provide further information in pursuit of a fix. kew-2.8.2/appdir/000077500000000000000000000000001467402032100135675ustar00rootroot00000000000000kew-2.8.2/appdir/kew.desktop000066400000000000000000000002051467402032100157450ustar00rootroot00000000000000[Desktop Entry] Name=kew Exec=kew Icon=kew Comment=Listen to music in the terminal. Terminal=true Type=Application Categories=Audio; kew-2.8.2/appdir/usr/000077500000000000000000000000001467402032100144005ustar00rootroot00000000000000kew-2.8.2/appdir/usr/share/000077500000000000000000000000001467402032100155025ustar00rootroot00000000000000kew-2.8.2/appdir/usr/share/applications/000077500000000000000000000000001467402032100201705ustar00rootroot00000000000000kew-2.8.2/appdir/usr/share/applications/kew.desktop000066400000000000000000000002041467402032100223450ustar00rootroot00000000000000[Desktop Entry] Name=kew Exec=kew Icon=kew Comment=Listen to music in the terminal. Terminal=true Type=Application Categories=Audio;kew-2.8.2/appdir/usr/share/icons/000077500000000000000000000000001467402032100166155ustar00rootroot00000000000000kew-2.8.2/appdir/usr/share/icons/hicolor/000077500000000000000000000000001467402032100202545ustar00rootroot00000000000000kew-2.8.2/appdir/usr/share/icons/hicolor/128x128/000077500000000000000000000000001467402032100212115ustar00rootroot00000000000000kew-2.8.2/appdir/usr/share/icons/hicolor/128x128/apps/000077500000000000000000000000001467402032100221545ustar00rootroot00000000000000kew-2.8.2/appdir/usr/share/icons/hicolor/128x128/apps/kew.png000066400000000000000000000056401467402032100234550ustar00rootroot00000000000000PNG  IHDR>aiCCPICC profile(}=H@_STD :EEkP :\MGbYWWAqvpRtZxp܏ww^fT2RɮFŀL}.Ls|׻>Ur&|"qEA߷_w\>o%uRRV^cՎ8 P?K#"VMH佯O*LicߏR~Vuk@VVu:8R\ L&L1§LERrrrqz$]jW(qqqv}kı^ou=bYrM{߼2NZǧ>jdS5{6̝'^[7ou!CEx~]YvrrB{ıgBpbWG /4!2<>-~`&ukի`0n# 0@bԘ1vn){|R"f͞ms!ص[71M( jtIRbAzJz%&mhV#]jz㍧Y%)ߺy& :Nr^^^t( CբOP\r֮ 4NO'4rՒti2h=c&/Xg;Q ,sx]mvϪ^΢L&(pQUY) 63gfgee"W*x}T|!̙7O˻՘v#2* UKrRչ+/[;[IJ -|'б&LL\)Ӧ`HÅs8*1uti2 gDUUr`?i ())mB(1!Fc>[|a]T ,NeOqܾ}[(ɠP($_^^o9"##ݮ P!߿/iߖmg@mLNN뾍6`Yd$(/@YY>z?HŐC PYYbѣvs8~H-^fp0_A7K[M PB PB PB PB PB PB PB PB PB PB PB @( o"pIENDB`kew-2.8.2/appdir/usr/share/icons/hicolor/256x256/000077500000000000000000000000001467402032100212155ustar00rootroot00000000000000kew-2.8.2/appdir/usr/share/icons/hicolor/256x256/apps/000077500000000000000000000000001467402032100221605ustar00rootroot00000000000000kew-2.8.2/appdir/usr/share/icons/hicolor/256x256/apps/kew.png000066400000000000000000000134731467402032100234640ustar00rootroot00000000000000PNG  IHDR\rfiCCPICC profile(}=H@_STD :EEkP :\MGbYWWAqvpRtZxp܏ww^fT2RɮFŀL}.Ls|׻>Ur&|"qEA=~V!}G H0 lBIddl Lf|;6In$ @ @ @ @ @@ @ @ @ @@ @ Z6m:w颉OfbDDFٌt:?[Pg|k0qJLLl5vtR^^/n6Lo5rCyyzW].tR €K7@ |\@W`ڰ . o/IZEEv6  ;wV:u-_2tS.LlTiIOkZrNlED]Nm55ղcd~t-hYzz3ի***tBNvUphKK}ϖwݣtAOP6ohkȐ=zݫ>@[6on:En~~~֖_/j}ݧrzjtVPPPqJNN:wJ[niܞ,/+o@ )Sez~~-۶?> EY,Ku8+KlEWth[JIIU``EM!BL‚((ȴ(+QJ$|֑ɓnzαcż!5$)9ESf+++nZ֛  1ܣݻtQ?2UtWv"?_ٯQwppt|E..ERFݭ~زҜٳ}%iGgqq"?_ ϟUq/==z'yn_}utzjZ_hXbcߕz 09S&LԄWHݻWO=9˲a4~yt啽L_;v+=,Z]8y}l6|EDF68]; qffN#а0Ѯ];+<xh&n[۶mլ3xLFM~~Mj۵uY/A󣔨h58={XdVhw73nSp9C$oY;L2n[֯׬3(]ޱcM 555z˴߾ftPUU^<*,,4L 4 5}6Mx`XCMFEM#nu˥U+W'fʇ_C4,k -[[e">!^znp' 73ӫku UM G, d/*EMl7 jZ1'?;˾[ez>6oh:M 2[;RR>蛙iLtǎ^IIħGd ..N.Rff?_]|-"0prvtYWVee;w0/5~>z͵ ,DMѹs|/ԣgOse.ӯ^^b_~kG}~>}|oz]TTTߵ[nh@[s{Ni?3g_k]CjZ%C2C<?[,0L )oX>έedYrwZg'Lи |N,  Q 1)$[~fUWnΝ WRWX 왑a\YyF5 wie4rmZvni߾tunͦ߷s6CAS=Ν=Қ5S\!aZKJJ2LKJJTd0 'Mc3#6GHHH>6M С, AAAOv e:7( ՍÆY|/ ӊ BBBڶmwﮧf?Ӣdupn(8*,0۵k+2 Si8ltf%W -b4x=-Z&Vi W24h`i4St8Ju,ih}u`mZؔhYMF]z9a8J+)9[q 4sc9pj/u҄=>cOfIIv^uʹXRX{̨UCQt\Znsgy\ MLL0\rҡC4k 'Xo;n\eZ^~ӳ1_fruVUWWwZZzUhr9] Եtɒz`mڴѨ;vh-_ҵ/\z(4uc=fR49^ ^aYg0\߰aM.ֽtj֭@jup-I'gFFW^?_sꑩS~VmW%K_=ӬVlzOS_6GirL.U\TD`ӯ^^RlM{l"""=Gi^Zt{;~C?j #""uwhҥsѣ;mLLxflz_w\ڰ~I*M77^פ&[摚fi=ݻviɋ5LvNǘt#de/,TII.L:tPTtBCCM;pNYYaúaiM^osԨz@\4olX дk =ukԙ34%%%ހu|:]\߾>M[6o<E J3V39b[dq{5ͦܢӤo߶MӦUr&|"qEA/?)\292Ϝ@l6~0abn2S]]c_By5A1oձiV>MIJ7r BAU>=pSC*v젢; 9AȤ: Egڵ( 9xe˗e q)rss1LLo V+*(~3fd޽D6^Y%)4az:E,\DKK [JQVcc4Fr ǣj4y Ex 1c%(X~=/L&mt11:t(99466RQմi¹s( t: "r9ccۉ .nc$L8؟o=rñ$$&|0  Y0|p<<=QSè!A:]]]?78f8jQ*KMEVqssnP\h<5~<8$I_nJ;.#c Fe66ɩ}:G!{.`fX_w'OrjƢhhlj:RSr]3 ˙9kfs[7jh4MĘ1 FPT4<Ӕttt0{-Nv\b0=''jP$&%VZӘ2 ???JKJ(,,DѢj98}R+V@yY_}vWWagF&2wknٰIsw/X;. l6JSf@j@mm-rj!|}³srJ>Dzi4xn|j|ҫr@AUҥ $ 7w^$1Ã$BeT0CAێ6ZR1J4t}=0B:{9>Q⣨@ @ @OY00IENDB`kew-2.8.2/docs/000077500000000000000000000000001467402032100132405ustar00rootroot00000000000000kew-2.8.2/docs/kew-manpage.mdoc000066400000000000000000000101021467402032100162720ustar00rootroot00000000000000.Dd 9/3/23 \" DATE .Dt kew 1 \" Program name and manual section number .Os Linux .Sh NAME \" Section Header - required - don't modify .Nm kew , .\" The following lines are read in generating the apropos(man -k) database. Use only key .\" words here as the database is built based on the words here and in the .ND line. .Nm kew music command .\" Use .Nm macro to designate other names for the documented program. .Nd A command-line music player. .Sh SYNOPSIS \" Section Header - required - don't modify .Nm .Op OPTIONS \" .Op Ar PARTIAL FILE OR DIRECTORY NAME \" [file] .Sh DESCRIPTION \" Section Header - required - don't modify .Nm plays audio from your music folder when given a partial (or whole) file or directory name. A playlist is created when finding more than one file. It supports gapless playback, 24-bit/192khz audio and MPRIS. .Pp On first use, set a path for the music library (this is a once-only operation): .Bl .It .Nm path "/home/joe/Music" .El .Pp Typical use: .Bl .It .Nm artist, album or song name .El .Pp .Nm returns results from the location of the first match, it doesn't return every result possible. .Pp .Sh OPTIONS .Pp .Bl -tag -width -indent .It Fl h, -help Displays help. .It Fl v, -version Displays version info. .It Fl -nocover Hides the cover. .It Fl -noui Completely hides the UI. .It Fl q, --quitonstop Exits after playing the whole playlist. .It Fl e, --exact Specifies you want an exact (but not case sensitive) match, of for instance an album. .It shuffle Shuffles the playlist before starting to play it. .It dir Plays the directory not the song. .It song Plays the song not the directory. .It list Searches for a (.m3u) playlist. These are normally not included in searches. .El .Sh EXAMPLES .Pp .Bl -tag -width -indent .It kew Start .Nm in library view. .It kew all Start .Nm with all songs loaded into a playlist. .It kew albums Start .Nm with all albums randomly added one after the other in the playlist. .It kew moonlight son Play moonlight sonata. .It kew moon Play moonlight sonata. .It kew nirv Play all music under Nirvana folder shuffled. .It kew neverm Play Nevermind album in alphabetical order. .It kew shuffle neverm Play Nevermind album shuffled. .It kew list mix Play the mix.m3u playlist. .It kew :: Play the first match (whole directory or file) found under A, B, and C, shuffled. Searches are separated by a colon ':' character. .It "kew ." Play the kew.m3u playlist. .El \" Ends the list .Sh KEY BINDINGS .Pp .Bl -tag -width -indent .It +, - Adjusts volume. .It Left-right arrows/h,l Change song. .It Space Pause. .It F2 Show playlist view .It F3 Show library view .It F4 Show track view .It F5 Show search view .It F6 Show key bindings view .It u Update the libarary. .It i Toggle colors derived from album cover or from color theme. .It v Toggle spectrum visualizer. .It b Switch between ascii and image album cover. .It r Repeat current song. .It s Shuffles the playlist. .It a Seek Backward. .It d Seek Forward. .It "." Add to kew.m3u playlist. Run with "kew .". .It x Save currently loaded playlist to a .m3u file in the music folder. .It gg Go to first song. .It number + G, g or Enter Go to specific song number in the playlist. .It G Go to last song. .It Esc Quit .Nm . .El .Sh FILES .Bl -tag -width -compact .It Pa "~//kewrc" Config file. .It Pa "~//kew/kewlibrary" Music library directory tree cache. .It Pa "//kew.m3u" The .Nm playlist. Add to it by pressing '.' during playback of any song. This playlist is saved before q exits. .El .Sh COPYRIGHT Copyright © 2023 Ravachol. License GPLv2+: GNU GPL version 2 or later . This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. .Sh SEE ALSO .Bl -tag -width -compact Project home page: .It Pa . .El \" Ends the list kew-2.8.2/docs/kew.1000066400000000000000000000072131467402032100141130ustar00rootroot00000000000000.\" Automatically generated from an mdoc input file. Do not edit. .\" DATE .TH "kew" "1" "9/3/23" "Linux" "General Commands Manual" .nh .if n .ad l .SH "NAME" \fBkew\fR , \fBkew music command\fR \- A command-line music player. .SH "SYNOPSIS" .HP 4n \fBkew\fR [OPTIONS] [\fIPARTIAL\ FILE\ OR\ DIRECTORY\ NAME\fR] .SH "DESCRIPTION" \fBkew\fR plays audio from your music folder when given a partial (or whole) file or directory name. A playlist is created when finding more than one file. It supports gapless playback, 24-bit/192khz audio and MPRIS. .PP On first use, set a path for the music library (this is a once-only operation): .PP \fBkew\fR path "/home/joe/Music" .PP Typical use: .PP \fBkew\fR artist, album or song name .PP \fBkew\fR returns results from the location of the first match, it doesn't return every result possible. .SH "OPTIONS" .TP 9n \fB\-h,\fR \fB\--help\fR Displays help. .TP 9n \fB\-v,\fR \fB\--version\fR Displays version info. .TP 9n \fB\--nocover\fR Hides the cover. .TP 9n \fB\--noui\fR Completely hides the UI. .TP 9n \fB\-q,\fR \fB\--quitonstop\fR Exits after playing the whole playlist. .TP 9n \fB\-e,\fR \fB\--exact, Specifies you want an exact (but not case sensitive) match, of for instance an album. .TP 9n shuffle Shuffles the playlist before starting to play it. .TP 9n dir Plays the directory not the song. .TP 9n song Plays the song not the directory. .TP 9n list Searches for a (.m3u) playlist. These are normally not included in searches. .SH "EXAMPLES" .TP 9n kew Start \fBkew\fR in library view. .TP 9n kew all Start \fBkew\fR with all songs loaded into a playlist. .TP 9n kew albums Start \fBkew\fR with all albums randomly added one after the other in the playlist. .TP 9n kew moonlight son Play moonlight sonata. .TP 9n kew moon .br Play moonlight sonata. .TP 9n kew nirv .br Play all music under Nirvana folder shuffled. .TP 9n kew neverm Play Nevermind album in alphabetical order. .TP 9n kew shuffle neverm Play Nevermind album shuffled. .TP 9n kew list mix Play the mix.m3u playlist. .TP 9n kew :: Play the first match (whole directory or file) found under A, B, and C, shuffled. Searches are separated by a colon ':' character. .TP 9n kew . Play the kew.m3u playlist. .SH "KEY BINDINGS" .TP 9n +, - Adjusts volume. .TP 9n Left-right arrows/h,l Change song. .TP 9n Space Pause. .TP 9n F2 Show playlist view. .TP 9n F3 Show library view. .TP 9n F4 Show track view. .TP 9n F5 Show search view. .TP 9n F6 Show key bindings view. .TP 9n u Update the library. .TP 9n i Toggle colors derived from album cover or from color theme. .TP 9n v Toggle spectrum visualizer. .TP 9n b Switch between ascii and image album cover. .TP 9n r Repeat current song. .TP 9n s Shuffles the playlist. .TP 9n a Seek Backward. .TP 9n d Seek Forward. .TP 9n . Add to kew.m3u playlist. Run with "kew .". .TP 9n x Save currently loaded playlist to a .m3u file in the music folder. .TP 9n gg Go to first song. .TP 9n number + G, g or Enter Go to specific song number in the playlist. .TP 9n G Go to last song. .TP 9n Esc Quit \fBkew\fR. .SH "FILES" .TP 10n \fI~//kew/kewrc\fR Config file. .TP 10n \fI~//kew/kewlibrary\fR Music library directory tree cache. .TP 10n \fI//kew.m3u\fR The \fBkew\fR playlist. Add to it by pressing '.' during playback of any song. This playlist is saved before q exits. .SH "COPYRIGHT" Copyright \[u00A9] 2023 Ravachol. License GPLv2+: GNU GPL version 2 or later . This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. .SH "SEE ALSO" Project home page: .TP 10n \fI.\fR kew-2.8.2/images/000077500000000000000000000000001467402032100135555ustar00rootroot00000000000000kew-2.8.2/images/alien_astronauts-kew.png000066400000000000000000015144531467402032100204370ustar00rootroot00000000000000PNG  IHDR_eXIfII* (1 2i,,GIMP 2.10.382024:09:11 21:00:16_HTzTXtRaw profile type iptcx,(IR# .c #K D4dCD 07$p,D 0 1iCCPICC profilexwTSϽ7PkhRH H.*1 J"6DTpDQ2(C"QDqpId߼y͛~kg}ֺLX Xňg` lpBF|،l *?Y"1P\8=W%Oɘ4M0J"Y2Vs,[|e92<se'9`2&ctI@o|N6(.sSdl-c(2-yH_/XZ.$&\SM07#1ؙYrfYym";8980m-m(]v^DW~ emi]P`/u}q|^R,g+\Kk)/C_|Rax8t1C^7nfzDp 柇u$/ED˦L L[B@ٹЖX!@~(* {d+} G͋љς}WL$cGD2QZ4 E@@A(q`1D `'u46ptc48.`R0) @Rt CXCP%CBH@Rf[(t CQhz#0 Zl`O828.p|O×X ?:0FBx$ !i@ڐH[EE1PL ⢖V6QP>U(j MFkt,:.FW8c1L&ӎ9ƌaX: rbl1 {{{;}#tp8_\8"Ey.,X%%Gщ1-9ҀKl.oo/O$&'=JvMޞxǥ{=Vs\x ‰N柜>ucKz=s/ol|ϝ?y ^d]ps~:;/;]7|WpQoH!ɻVsnYs}ҽ~4] =>=:`;cܱ'?e~!ańD#G&}'/?^xI֓?+\wx20;5\ӯ_etWf^Qs-mw3+?~O~9۹iTXtXML:com.adobe.xmp bKGDC pHYs.#.#x?vtIME  ^ IDATximuULw|&"[IK`E$?JlX! qH8 +P eIvlKeREqjd}yUQUMR6wϰOUַboe8bWħ>~|/v]/8c׏'?g?Ckuo}{W^~5\G_ ٿEҿux>{Rc#u,{,k<̧J}xGgs{7ncgIp0D{c]?Lm";O?>$ 19:ʹsg!Kk׿5<8|;|V뛦GcVW\˱+s8VΝ#'Xp1`_\z~\O?s;o_Ùd[ Gsuv[Am'p[-}o]g.Cmmsh=K+ =IgYzn. KϭҨ'Nq^w&6}]݊}}g'GVW*OHè"Xpxm-_nY{ܾg`{|v=}*) 藃wpߓ;ڮ9)De~_}L*.~>w3#gd˯ eMtpon+u Njyػq\KE8w>}ZeSw&6._튮bʞ[8ܳ`Xx[7q+tl.FFX9{L]Xy>w]gԚfi*cHSl.}PߋJ)qtwKӟnc+;`Uqr| flneEdvednu8uk.V[VΞ&!;\Iv}㥔կO|"+=JO7oiΜld:E$zDsX`e2g] y}g1]#׏ ?g-l\\-T~۠8=m3҃xaUH!ppvp8´/toYϜ~qY6VΝ΋41څ hkr'6| vvܨ]?W9LVב#pہ4we;hMl\C3]q}&{w XYJhk}M's%<n|fG'?ܺ?b'7oUdne4aEZkjuht0z[3qVRJ\}:{Lܿ3g*7 a%)~?gG% ˿OY?/_҃[19> Ǵp_XWħ>^9+ S>yJ{o㩿=e+r`}\x}?3;~ߺ>;o| x'՟'IٺUs*s9ŃjWݯG?{\m$ k(`g1F#؞.Hp niF-G[ۺ!y4O&`/BT&fW w_o}p]e_^o^᝻Ǚ' (2wpOe|~opn1+q|[ý=MdIj<8'{%)v w&[僿~EIVUI#)) w`nPV ލ YF쥄4@q,I*z;qouFC,+IFi^4?VDf6/egnj!|? motS6 rMA}.;յρө*?\߿,+1͟2rONfYN}_}MލiZY)yc{^E^=[oqGńyy;gZ43I n#Iˋ 45 Sv^3̢:Ęv{Q!L,q4m1!m'~Y94Jjr#$tۯhY_B׎+ۯ^nFwGLHP(Mi/z6 4BrڞG/5Ӛ.F{4d`c|txJ)cXp􎔒m4ky d98Ii`"# #XAdѿ^~x*o^_x^B/7>L!p/9&n߭8OM˯ G)D#ZVWhr"M X8/ih2c+]~,\j0}@ 2i0+kB5@qåyEwshg,e; Drha  5iؘ)M`zxLjMM6%c ܲ*6 @B'8Lauv/)BT1RRsuژI|tFHSr( 4 ̸*T~|vVЀ/D߇"Y&$ ̒ܲ nE{ akgZ>g`5 qOMdRޛojKi9cC;__kXx~[Tͤt l)PL'am'GholԾ3Kۦ$aHQ& զտ;sFs)n6/6~4t^u6X'AxFCEff,(PӲm>KGMH<9kc1$FyZS`ힶɈݐu;Cfٸe׀-)t~QW_3lש9>9/nedJ&eضtHn{DLe^}{jSn# I!4l|ΜS봵e m[cq+=j{'Fj3ƴQp{=JtQ "WdfU`kKJz:>ϊ :\~iv) e]2KHsɵTQ@ndmPeT@4՜aF0F@>w)Yv>K#uz}^VΝlh.01M!h.kRslrO(;mdS>e(q29nCBr#lY=lf!eٸV - NJg7uK2 fZLsD^M~Bp˂ cfP?G>SX iqLHn>6izy> !w#(3zj^ -OP}r~L+ j |/ZI* -/ք,W7d߇{3Li $=}JcҔ1@n+ Σ#LW ~B|JqT> Wϟ/ فEE-MteR5*h4&Sa.V$h( ȴi(C`FH!ddL(C.5653{*RM* +  w_ X&࣢d:= | ☢M1IEZ7ʃlE^_R)!ӞR9W/ʙ2 P7m Nb#C%# i)AKz] ~3zR훌s}X7NLkrp[Z[hx"k/TRl{gNW<R6Zv)6VQl9VtK%u0wuQaY&` PPL,EԛsgRymS>=ˠetOmAm~W=#BN3r&AVK>mK;k(<dkPmE2Q`iX.)4"fK= )mc|tD l#)l TM ش?@2 9uj{oVx&HgFc*HĉQ@H4Ag2|AiSĉ7m8eض=L1ˠ'#YrlNiO:b,025Ps t#v0}r%wP@e@ɘy$0EՂ:m2dQi[:(AɃv*Tp@bٖPZ_##Sz8ܧ7:rsZ@"fnS@ s~NDj^z]mOT~ߐbOG)3D$aM߫+n˶IZkut)INݠ" f=Saq˪'qN~"S6 %rLzQ?w7} Rd͐];W$4`ͪ1P) 2 l;ƺQ,NizVۚDJ'VV4Jtm \wo݀]Kg8I=,AMEem(֝)$U͇Pzg21}mAI&=.S`)펚3-f5-jilsg4b '&q=U1c0ˉOD멽a p:W*FP$v CoSn2چ:I-6gNRiRHT=Snb@Qiﭞ?aP{Pûwk\,d#%u3QK[~OguCMWPsf>1-{gNk5gN `Egq%Zm!.Yú̀_׍hʿSlYE,t`5YTVz6G~Y\(S=M$9ͫ1u\όMZw[ <ल:Uzcf(`%_<HThgKf:)nːXtMˌ7ǃ|D˶I.:l)d(ϴ}OBӽL,CM3r4Ii`mg>O촺t(4W f1nhtNn@+*Ҿv/U~)VTZ: ^_~_}@ߟ?3lC mVXU $52o+i܋8,(\ij|jN?Ůln"茚۠G[ZV9"Pymg?߯i][#j2VG!o"%"U˄e `XŔ` HɁ{d9^H.0dYt4m|klPvnRC S`D^b*ɪ2 Z-M:D&b{A6zʥS`)$^s|]F_g,vt7t2˴uyP*Q(h g>.S:q4!ܧCsilzJYl łT#j65.lzƘ@pE&CmWk5OSku@:>3Sb9]:E},oŒU*Bvpni}u|.]X5 g}52fU m ]QӮ8\G̙)hrLYVWl-k&[3>(]awB_|.JcUhgmhYT; Q{T'0f7 ۞[ ͆8yveDMF2+[F4f]F9e^j!9 \\j/U7el Eצ#UL(c/{̌ǛOZSjy~Z~ ݽgx C>y֮$ Gn R3Xکm,I~ x0GM䂨b5j47Q1m-qiwI4F \Sru"3ȼ3Z5. ~"{7TmZj,7;uNmVX\Mv{gOk)34: fz9D<9m|K]}2D}mgj.UZlXL7.{[mr֠8BplJ! :{`LҔ,X>*{d:(o q A?)6 [EmBeftp@׋% ^e +Q3T#[nE)Cv+ '|vyދ,#:̶4V+u]M/4`F(\]V p\3L;Yd^3˴0 Z#::Lk`, nFɡ\'^C,XIVK[iNa}Evpw~k87k.3~ݪdx~4s}ϷVWq'=i2zqEN}33>X #bfbAU$j?POp!Hd FvBd3hH+Sfm6*Ԕ/H۲-s=Yh(*]Ӝ41VM{|A^RjRLu j묡UJN(cp_)' niJSWkuU;f)ut[Ҹ:Yukm5zɦ?~8OtlmZ]n+ЌM ղ}8פ=Tu,I'N 4 *T\2U:P5ل+6> h{{]~@2,ь4λgy#`{YW`ӧqpVϟCkm ~wBb}838Z;no߯8u^Y怶mẈ[8vZMk{H)J}D ,%R,I4m씛gE1ei,(L3SM8PcT$aHk6E8V9 GX9wv)>_pK+4r݊}%v\:&/X]A8@DZ-^4ࢽFu i1N2̲*+:֪Ƈ}3:8n#Bxvz|7ϸz^O ,Pes<ƕ+}Mm.a'$[( Xqs]]nY5z4`guu'3PqP38I)1$Ql7XSF6d$>/Tgx<{h؎ "D4%dGojp{7nVpY`55p07T)k5#!\^/ m\=7cf[WX/z/Ij6$>24V.yEtź=o"ElD"OQ7 @+I{3ULKnF^*x;AEc}R!cvmQc=i*PBT!n){Y8Y{\ڬ rM5vg:wQ2ҸyG_Eqd#JvqHT1:aK5gLR3 rfMaYf}10 L"Gf$PfNH#Hib1Ijb)YDh$N(PL5Ols324,]>ǔeQ^(Q_G)Zkjuh.[<UO&p[JE*TJ XQ{D3 XKF(BPj3l$ᖅf݂n#L g|0;w lݠUW9hok~<4+ƞ"Z!. ;*k9ݜ;c vpgkkI~^K4w▅K|A24P^srZCP*A2 %:tx.ɨ`v=MgsPWtxc;{3{$au<-d<G3V'q `jfU [:s`u.q'kRq݃CΞ+h\ѵK+MDV3Μ3 B%t|x XYARuw..<\Eg8AZQz:w4r&-'ع<勈'1PMs[q6Ե @EYJ~]QL~qUjB9XUa^COJ1 A8w!Îl3g` D]!)l8 H,&p% oƕsozpW@o]y -!1ko̭ )%)p8C D[wu[ʲ ;o+]JZ.7:61Nř"}z6 ]v.Ȅ()%8s ^yp9`~AAt8E2?M%m}<;C#l~{1@Aő""xDufR``R2eH2X`[*6[wuJH@HD ,Ɲ_ƚmWKHoHHHI FH%C{3hH7#1l嫰-sda-A&UgT!$Bk$*AB&ErAmCܣo&Ϸnb,[ed5HYœk[vܮmztGfqUS}o}x-q#M&_v cސ:vK+!QDETqɞDM23P5CUS?ûMgjCԜj%IOj(Y.TJSZ9f[#ٌTidQ JJ]7EJ<4D]aw"}yб{h0s*hW9xEZaִ|`V20VqL/]F aC _PĞ[TtQ1u1t4.6)^%$ )E}n0mf6[] jHfi3КJQASۓ0ҜҺL"l^8{Gdi $-εw^[εwJdxNa;o_x2U)R8Zݿy :,|-ۦA(1Ԙ {ipCkpkܪΜoTGnqLamph: +تQV }^8_0>Is!m`x23.$)^ظr9cu:]/O-S߇ΝmH)(,jI)ѿ@9{|F$%pN;iAdI1 n+Neݸ^4ąg(D8bC!x|sU97g=ܣ] ϙj)}y޻ײpsIL{w8BK˂d O]7 'Xb3Ȋb* %,Npx.2! $EtG<  }FLJ˱a1$E%i)9{m0! @4Q{%R ē ^[ȱnkNYزglH_2Ok܂,d2SNܬwwbRboVBwnb^NpLLcH, B4}j32IM&`aɳ+b8_Fݏ.}v41ҿs#seY"rO VȞ*8XnI;y,0}啼Kw~!FEs,YhYN1kpkP QVL2.2!ҿ|8L5rdY3ecӳq s:cpCe m$vr9(G ;"s\e4®\g/˵(Kaq #$iVQ`4ַ`.\ׅmӣ#]M}koG(1vvv{nネ9wHٻ4v OIFK' jujL,h}Zr2Ii|zk7{R$VQ W IM]Ûj5F_612zM0ȺYC[KۘI붡Ξy:{Vpn[umN67*Va55Ϙ:q.u*yq YOMڮq;h%N~HlڟaЙt"$AQE 42s@c0E2F(H J*&9 %Jt>o_5<#8 G}\qo?SB`rG<浪挃d"cD cع~p;0~eO)q$0 s?Y`zG8HE7}׷z -؜c:|츎m0:r\` pae2л--eY \}{xL--߀X-}!c=y\:V0[ cjEDyq{Yq$"poj¹ؽμ8{7OÅ|3uo N?>\Zn:vCN`R52%>;aC8^C)K3Ae` Q%cPTgL":[& A+PP][i{fwN)74S5sTr F[bNT+ܔސl!KEQp˲H+ zPM1+Z+Fci{>4EZ^keV1^/gmdb!a4Yβ[@gEzK5r\P0 ,P'?ge"=)PQ3T]^τL)FXi"+zmoSayHKrf7oM$(@@$x8S1M+j:gsН1o􏂗ДʿkߝH綸L~XYG 'lBS7 u1lwmu968-.̀!bQ$;1b4?*B3e5 "3),q\+mL%0 |`R=L9{,JB+{ kσḦTAāԆLd8H-v~^fTG/qwOmӏZ3FHεazj { ׽$RlxLZ4W{swsp`]sD2gGNNۗk#X~ֻaZ+-`E5u7+6.kC{5T>_x_._5/u$~ c`1 1zLS]+O-$M~##aiJCc!+39M]' j4ADS?\Z:IrmGX3eJ ~?_ql | B"3n{j㡅zx=^`C?7 E &EImcRb ,/ּ$s ,:i& n"nG.[. (IhdHyLRBJK2Bb8GRB_;cat )6ѭFH GO~C2•~?AUԐydَ 5̀` c}2g4Z]=tT#{(?}7gRW 0oWR:|}`|xH>>e |dqg,J_`F>9fZK\z6XϏ홛sȦ}^#L뙰p6ڳ-XT.hkb Afkze-hMlr˅NimI3M IϦ hZ ZȖҔxI YG D`tp}xS pL@"2vS:ei5(InJH9n8'#F'7>s?G^xvLuQdN2福? x* #L&!V ^ӂ B{=}Ѡ[fc^UL a!KU9)&Q~g0DILHU#*$B xJ-j>HkҼ4󹜽Y\,͐e"0LVDi~!Cߙ (@rc0HYtƍ$18P56~5o1BQf߳B HY,&)%DK$oU53qƊrgDi8Ttd&)Fa8I Y/ߵeddErMe1ꏙ?vZ?u`Yw8s8@S1 RaLES3 ZuŤmTM"u*l9ŵEH@=c&)L|R"+r3R2dz`Js|42&U2sjmKǬa`E/^A͗"ҟF&L"428n[:!VcWw;dy:T :58N@-BqNmGUFI-Z &C"]R'~j!fRo4:^G'>CqQwJ,^ @up?[U|6Ӟ<+)˸<Q N0 XpmAyp L2Ic &S8t"]υp] |VQ熗?0(9ΤT΃Ii aې `&>;/97K s&(9‘BBH)L KR(Fa(I(4$E-xr=\@~ 2Ed" C8A`q0!LܡeT0 ہpm 앋pTC Qh.^:S )ȄQ,Zva\3Y̡$rdeK}-YYEYG+*Yd}b <A-8۲ sb6m(°敿'%,0D*Yɉ] uRfB/ٯ_H.أ,g ֬C5c;f &y 8L`UMlr$vJR(gx{{Bϛ_= py("etfV[>,!0F:,0`M/e_ ͕;9nxk4ͪ_M Zo⽳6v'9ӊmN8l@8xwv{s8xJGhi·߇=vӭB|{0h%07na'_-[zo~e S~g+>O'?"Ƈx_}v"$S'>h-qrکּ0J9yd%F88K 윽. ݉DEȢ}\S@S,d)Dr[LYfʎ䩢37 Sٔg:;1/\Y:KN&iSi8H!x:Az?4n:FTuɴE6+W/hxR<;4e@әQs;,H0(smT&0IOlY1@35B- x K3c9ձ{fԜXW L3tTlD)1gIͳ;Q٠oؗIS-%m~ \x+hT:Rl_>>w&vp7󗱿n}xRJ +7g?ɒ])(F!cLw p\D̂`T&りȧ?@Dxe!à`@ gS| 2` dqJO HQĀ `,˂䇟mט"|懦  D1B=4Gt\K$Zt.jʼ{WʖI~}IeI %NrM灟ll:Vzc} |Eʂ )K%^BտWK)jYdU{tymWytof !F 06ccӉnPqU\H*vW%t)ۤ;m I ĕt3V>gw}>]q.ak~0bs23'}~+(ο'>dߗ&3|i2}1E.c ƙ3x6;f`|X Oa8;׽;OzkْG@N%brp|FBr6ON)M(%L l4l=/yB$ @[*\VdE>ʎ\x7)?.fNPϿםLaP5J`'Ue I9wPN18\z ]Yb1ϳ%f1œUM~bɧ[yr]u+_ٯ\1~/%O}r s;<4Ӌ ;BH{NQÖ+F |O~"+ΧM*2xV3?ߟL1UY. iey!%ỳ3/L4}t.$ @]V@fM%1U0._w?WD&7&"JSc=2MI\ŨD6$Wly&A6J$M>])aZ5J}f C^LzB7RBCT= )]ȗDQc{RSEOcHpc׭˒m:LZ@J+'j8b_BiJcAdYSٌTt (>io‰׼QܴtS<:O''~x4Z[ڹo S KR.RߢZ*;Ela]_vI! hꤹ' xeFBZA@Ok%Bu]ae{_Sڔ΂.w&)8gqo$ u*IYevz"&5&8t*' _>4f>s~|s;m1bĈ4~va~k)J@tꏁqxqp/?C>i27xҼҢM*KUƘ*CK&WJ*GIz|4toe޾z Iŗt P>Rx\p\GH Iq;\ETGs !}ZzYe%Cr IEvjX}yB,`H^rI󜮹gÕE =8gC+g,zPwH) e]G5p˜;5ѰJ kp' &hߣ8OM̞s#`n~|~_XΙ]|哿obĈ#F~?lm/_{Ӥfc64l4vd9B@^v `S ƸB)Ï 1|/_,N}-# -a+ ܜ<#\rkW[n<Ƅ15-c4k" ^BNGg,#+U`jKkr ڕq۠y4i^38l9/\ɻUj10<倞J$!˞ '9N'v ZVMW S;ahŸhN1A%ݸ}t*swXyI1fa007@ΤeLW48O*P+&pp<6Tmógq-,sv9uň#F1b\xw|~ǀOUhCaRƁ`b\BQȞs3>-w4 {ˆ~$[xyU U C8 G8ܡ{p )   IO\WW̹ 3Jl4|1+w t 6YHz]iѭ&ۈ1oI s]P.Ĝ(^$;\;Բj +C)ZM2)2U!EJ`oC2arMh )%Y#Fdm܃}WR!M8I[[2 Sf@P5t ,[Fk'dIд6-T;PVHOZNrm;qi;=$#efAk@W@>Nvxj0º;'F1bĈq+_ .Kcj}6nJ~wnԚҕYťڃI؅P0= 2IvnNxM88N\sb'togg~4Ĉ#F1.͸vKy;MJG|4v}[B;RW+'6a¿?ݶ [Af>ARZ'-<) =z:. t'6$ |T c~*XH#[㑯H4kb<~Mh ^+[0"2˗SO3}+&CLXYe$ͻ8->srV*9lnwĭ%E5$卶,w[_3OMVM4jWLǻ rRI[e/a04̅4;l0Z`5T %k#.3),ǀs,gDsl8`I/dRjE5&'ӏG1bĈ#%I8n~"-^×(Ӽ" ,z.ռߕBǷ_y־p7cL`dSꥴ\ϸ4=" 2) !4Psu ْσ^i$c,vi KeJOp\o_3gYG:(P~RJvZ>+HEP\P jab*7,cE]49%\um *<I܄]q6/ȡ F^7:3g;L 0à_%l 3X(X|2A>-_!sƺٲԦM*^yx~_k̽|CIK b^ݮG3x!+\x#F1bĸbkHW? q_G< 0)fn /BEL<8^qz%,'O?W R%_5Hs=(e~PǴ{o)dՀR\4C=e8b0aT|E OJVy5}Io </:TiӼ%irSz ub+vp"bnMQ쨭&dXpbĀ*cyJSMbιN2lݦ/ ] =i)d|ގPKo3uf$I/Eo  I|2A-xdo u=&[s~F (2ġc'?x#F1bĸ 1O|oBO.dÔ^~ԂNmy7T _!zM+CnlW+3N|; YO-+ZySF6E#Vi4r5%܁˩ӻNߺ&KDT=Cy H2 ?Z]ܕ/+eF0I d9e`gL-WX <| $lk9 Yc`!D6E8U ]d3 +K| RaeN:=-7!N)IPj|j6'Si[W.bcjWnn9;8hOvPll˩f3y Čm*2',b{.u`E5r`8֠e9O ~DO|롇2ĸ"O4HRi Ҿ$Ls J#FK=664/&JzW-@0W;X\0HBAg=7bFߕ!ogOȇC/_.~n :(ƸDp(^g畄(ṝދR)_g/UC–iŗ,'Ўp+W WA>S5 (]Hi3HcVkߚ+BxUvY,Gܤ $IgtR{w h`(NT!J_c]I{@ЃZX);=bvHRk2IG8S`ty1jrx$ Ue}#X"wSBBIc3IYsk@j:/:RuNtmkYnA&x_}?@{L)Lrbĸ'1>u=S7`u0@ol! d98$;$䓏7P})K g,wsC*ߣaj9ybZvJ^(<'˲ @,lO9ضxiKdQ-NXoS9!u={WlS`3񜗭qAzJSrpfۯ^R DR.o]!أpp,.D@!"%yN]ĜrBUb5u\3ʜpt0h j6g<-1ߣ'WbBX+fՇ&xlFx5ĂhvbUhr2f l#$y}ĘS8/%N^^xN|20@7@kcpcoh.6}_ƙ _a[q[y OppOJ1b<۱9KkB.4i]K[^_[sTΝ~TÝy웘/ɚ,TDj)tX$L*h $B,P,J!u:mɚjuƩl~lU[RLH!X_~%_0]9!ṐPs,u8sgz f m7`[ɤ7sRid2ߴǞ6YAx8do[C=8s fI zhM𽧞h8 Nh^1sc9Gݘ6'>\NrUB*S('OMͼzͳ}c@ճ{=h-,{/u6ޔe&ROxipy7۴}}+$0:ϻʑKمN<]c$,j.#RQÉQifHUKNI~ 6`9uD3tdUlSXbӂQBw-ʈhLJSQl,T{bgQ«8> ;X{0n3]֐ӭc4#SCE^ ^m{hFy 67V)XHt Y,'k}%V?ȢUjt)s<8؉wk%=|#ʫV !nK<}̕uC\JʞR1G  'o7|כ1y-}6xhyi^ E.u\@5n1@1"z19ٗ>?=|?GȠqf/1b8_okُ]?G$Ѓ 3 º|Wg"3=;Ƕ éqs[݌_vz[18yקq XX@P7ʆ5V*jm ӗ }~~P}'^3zx豯#FGAa{ K%XS.ˬ̎a#*BJSni|Gá3d?Mps$H(j)gFuِ(`Xl4)r #ЧH>9|sZk~;T*X@:.PZ )>0a|! K8I?9`Y]=$x ɓd=F⯰;03TH ߑ:а1N@ who:(PV& ]3g(sz9wvq7 IUѓ:ԗnɓ΅HCIzOz"U6JٌoĜ Q |2BT4hzB]U1~xtn‹~q8o𯝋 aaTJڪү{@~53@eLOq:ې-/Ƶ{xGr~I1bDykH) < Ra6@ tw@>pԈJj7ZA?"M``(wTJdDe^Ji|9aPyz6(0'* YYQkb? IƵ8T$UA݈-'0ef7FZ_<<ώ ' \f{#vkOQoc |lkS{\˝A. &YH,}L |0չhF#L$)'SKTؖ(O $a`Q`hG_ [(Eʇr61 h FQJQU=p>i3nn '{}UvZ@ vL1֏܃uڿ7 2Iz\\x}еϬ]*m|A KXܬz Vݍg < G3404 0Zu&quF8 B%l%  Z:Mn>yͭCҢ By96>7 NIB(" ; )`a;=K^Hc>*KUuqb"g2::2I[hGzA'RB(5.<>']uB}V#븐iKlW)0`XdG#<12V&LütP`Nui Y)Tt$d-{𬣏}7bW'2 .&Ժ$eX`r$WwlrE>W7ʋf o[,aL5^6^~Mf߿z0ME BT)׾PI<1bĸ# a?n /|_K%g|#N7^t=jfpa0?bRNZ9@e @X }HL͛*l@e3RhHr$<`8}5`gT . 닃MҢӽ~qTBkC0 H;/W?#!ĢEٌf@9 ڮ ɪRsR.܏Qg: yt$y`@wqcsgu%HIh0O@ dD' t^/ضRL>k M HnlŻ9QjQ=_5UBЉ'{f>+?%'̸ЯY1ih@?4Jkt^?~% [}$ln OC[ #U^{>&~ǀ,t7U!L@Bd_@ο$<4)v#@RǸjB*L'2JZiQY?mڜG˭iP ^F:|84Dq=)<HY mnϚ=Wyic\)6֤3]+;%82r M?/FqH'3 :^gu9'{TvOHF"YB$P% (~P)CvjAA9Z%OqTJ_`0`O(& i[ǝ Uº"F*1UyP49w)0FG oC{7qgo. ɇsۣh'lڎVU e6`4.ľ{9y#mX IDAT,1b\! 7] |97[:Q`!7`> 0Ȓ^[2jhUD]Q_Ʉn7 JĮ|9-)uXlN 0$.*%Won_5lvHLCTö⪜yݷ[/{}\dnP7c;2<Ŝ`^N_O_,FWl-0S?!`0 * aф(KHyh[Pү؆<^ {!X.V "e j^I;@C@ǺWdsVԪ,EUF5 .m 4-e|$O㪵WJXPNg M@b0qkWobxA_z 0t ^~`%hFuZ';t.\גseA#F+'^p+5'ȥ[KO20gA]LЋt_"l62q@c 6' +IS(M\m?*یqn*MH&tlm#`I!^ຜvl4t9-W]Z^ { `s-L@?|MQa]%u(.%v#SqqVjכaB,-ȓ3p{}"$*Ia/z:I3>kKK~7Pw,(=C؋>QF2<h&)C2Ъo-2N 7aK7G2Y;rX![Sfi2hynj믆V-zV0._ &Dw7I$I  pO}8bĈq˾8sS8`VT{JL:eCS&9ȕsˇ"CYəFcarrWjIHAb4PS -=K(PIG;5* ;2mo f>k <|F dL)m=9/C[^^s/;3gx̘ ~~Fkm䡡JSolբhaN:}Y-4bIEeŀU էSB%1T `H\]U@Py7"=Ip=+Xt֕\2̔<5:ԓ`Y\UBIv7y1V[1ປq/uˋ.l򅕂o+p v{ X!H֥mcZbbe#Uqƈ_|k0_]s?8p)u ((g:WKnEjY[c snʕ{l!ϼ|Y?Rb$88 q`+|m\4п u#?(Rb!{S%nO;N洘ʖ'z}4G {p9c˜iq-,LXI8 1\ьM`Ъ$e˖d7lym\޹5beC vr/&9_.5՛n_:fȆϬJCp5) $˂OQ\ylh=,ɋ'#A}6kQV1vJc\y%^?|[wp8_\Y9.΍5( +e6~'%x}K1bĸ⯽_My 5Ïȩ4|_5`Jٿc\!R8kƐO^fZUp 8H~V*A5?"\Ń2?jBF8P>%Bɰx\ 4-q_I[JW,krm/~ڭƲAs ` _cNʀ0Ѝ!_6]qYRYS\$ĔJ-*گOvvY*]Cmc#ùs70\>֚v4枌1M:L}#8w}^|$r>r)@n^ V/cpPaUU~Bh`l߿Cluz e^[??"#e>~?n6.~}ey!մaz 0FE8&R Ki[ |n_-8j Ef~bcÝ]fZX|ff>$R _GzG>> 0LTI6iX\xVH6#'puׄ Ts=p Áh)Q[tc z0rƦ vGɲ$L@o~8AӘ+\_} AHCqE8A6)!M 3B͗ iHֆ{ t"a,jjbgfl 0T{R%@oTdAx婐H&Uϫ=F2UxŸc`w{Ǐ,?T w *6 llr 60(*/Œgvq?C?*~׻qƈҎ7_fO!NsNgzR k{a:ʄQ"1B9{ZK0gTTNǝUYPą}itJksHCpN3 *\?AՑT~mڟg=l]iњ{ʛY*+!٬TJo5ߘxa[W%X\1Z*Z1|N*)KUFy,0`7c(PR=릑Γ!SeRנSIjƱʂ!=c='IZjtdP<,<8gAMw'皪JEڢc=kM )/CK4dc|/|ߏ I3 o Ͳxi,ˀ~Ɩ7T1w]Ǚ$RrϽ _a1bĸM7"GD=7=\1Ei N}AA0Jaϝsɡ}P&D>7uEzzW*B=UO }i TnTeg]&<T6,vj?Pǘ01%Qcw5 i|F)p-JOPY{EbA幔C fR|DKmJ @ ;O/999JA*EրEkԲ,C5&.k1s$\ 6LPll$IH7 *I!woX.{HúCE_J\wnRv6p ?vo*m6kZj:`bȱJ ՟ <q7x 1b\ZWYy7[e϶C**?]U#~tx/OLNjFj>' kZyz*|XFdb88}ݷCm.e.!Z+_|ccfEU(h,¼kT0\ĺ7}il ܃V{Jhp1``JҀ]X,nHJ꠹+L:wvPkDG} E[wuK D?!ĢDJER$HP:I  Y_MU1Ҽc38B֙t$@?h2}ox0F8N} xKPy0gz3=~5))#F8.KL*ҕ|F)E|FVホbsTTUu pJ?`KhE D p}8!?Ke\2~earf$QOk |aW1c:*fLu.J:yw2`%ydTd)!f4QL"J)m91@9r:Ձ}|O`I|Hl!XIL& ;*y1sNDm#Rjul?Wd$_ɋ{5{&}S4X.^&`\j:J${}=D-76Lcf)ۀo:F8sx^@oRKTynw.]iFt;K0?$PVsQN2L px5 $ ]U>'i`svzv]~pYi޴YUs}s/%E=]Ng70eԥV5!И2D R^d?Kw=~Pp}mqjC*&k(BPJXJ($C>tU% Kno{FCOʭܗ|35k`<{  +ˤeyS >m;cHClx:\ Tj !^c]/6r$Jk;MGFO? <}- U*z^{ש (ɸ{ JCI. 7#F 47y|ݔ+TJ]*ib\[Vd$Q1hP`"Or L m!*-=Vɽ.ݴ j@>m^G"͋~ۚ7]jAOQ/RԺJ=}qkUF_WTL㳃daΗ}-8\w_{ +g02_mL[gVf[xV|mx0|nk#? +E?2FxG. d1b8qo~-߆scNr||؃{yhF(FwobW4Z/Q*u/cwk\o{N *66pO2 ھjE*Y.]*Bܚ2/!/i&,F#"ٻ@hH d9NH䊷Ѹ]<3ڔc,3x_1+9v"rfR% sOʀ>\"LxؖWȀ-xt@!U18ޔ1%u~)"oֹI[3ѐvNe2̜)9-d'NC_wJߌ?__$̫O{US ⫱R{fa1_|f4rMm ?Ol~Ժ`/{;y21b8w{q߇1{аLo@@gJ?{X IDATbI96 \5-'=ڃJN0yád-"t"'>ʆ:8P2F~ٰ&lNND=G9 2'`jf>ȝ<)r캗3~y`ɲ$Dn1 @Nh2潖&k].|NeYaFzLP\Í@fBP>BU{pNԶS $|2A@'!Dw5LMA`Q`>Y۸ $Ǡ \zr:Aڀq^F iޟ5u=oS5.bF̷MQ,>W-˄ IJ,B@zh^bI5<%e J_x) )`#y 5W _p?Zsnei6 цXxU c "Jk@+ %Q2j%sZL)Ѥ0І`L-hO0 <(pt2#cWK~)5JxƕlmJRV~R)h'քsUQ|=δ̅n$Zu9|P`pln# $dI , H4%CXE4\ S =BJwKًW@/H1&<3Wl'˜=iGrBOBIh<T:w2p{ Bv&d%lNO JS_`hfU3I T͐Yk%#m7.l/Re i֟G{[jxW5rPx}-7BdI,+@3^^ 㠏#OqhpP5f[X8c)擩?W־&\6 8s\IVVx"΅7~jTOA6`^!_7ֺl;]kϻ&8[N (jDP(Iy,$DnNI;#3Vaމ-FHF 4$6$sM&Hv 9S`PC&pcOE{Kɂ${rp˺Wm霜-t"Rb;sK)qp zdS1eցͺ`$C7ba)؛l@+=y fIݠ]IE??"j "-g3 BKb705V XyTAl7 AEҁ} ݷW<8cĈ=udq6 4cT1꿥`g1ed?|Fj 4H#ϱ |8qmǹIF; 2/*WLڼ2 #Ȕ,!eռ76hFUVkQ}2&pDŽLxe}<`Kx,$pIXWqYq}Q {I&܈HpN0c%F?~"lcT낯'9HLT-}V??IJΜ ПׁL7AAXk;ɻVW%TZZqEHHJw~57^k1Ƶ+X=,[;95hϠZ(kP87  wTކ`^vau?}PibĈqw|TW̓{Ŗ,bnހ)c)+TxL3$yN BQӃUvcsIr0$(660!*x@nUQ="gR2ͳ\j>J SsɮSpq s: [5^ PEᕱ[#cPa#, Qpf19W^_x0 ooSb>ѥ-V1c0M5i:/:llu9 yL:%XaSJ>ъ14UIJN$\/ghTIBO?Tܽz6$nBO0DŽQ%d t6D1b58/j,m 4 iX."Ud }F!LP7InA1 !}x.s̓ zߠ]{=}G,z1 Xizyq9{/ãW`֦74%.iQ*BʩӢZFym66vWe$=<Ǒ;9r;|XxH>|V9O]\}ժt=g~Is[EAc<5$j-I{n86I}G#8sҝcNkrMDqeQ mFٷa:T\K;T 2.kz<_@,`^}T.Y6za|_l%T/ uy/PFADd7^1E"IT<Wo{J0Ǽݧ7T.R>UF0\@ +k >";SM%VH| s;SWg$[=BDJ H` "Te dIL546?D+'Y%B;j{Z9WlL [X+. 69 ~l2o\itqS簳2&0/TehCfYp"㈡$T2C^Nh`좒J )UkF:pN% y\rהmw;*Z7ε @LR|-Ի1 *Ip.akRЕc %h\p"j`B}*m޴зr{[ΕʻQ9kW $XK\ѐAŸX"J?<U6eMA&DE 0/Ef3^` b2Nڼ[BE0@,<~TSL4bǝ1 I4:dWFcuЖGR*Ƥ/d.RL% 4 Hsse%QʃWnI`u[@$dUc?fju֟uf8uURx*JQ5\Ã8_nx @%jxj 40^̧@lY)cwp L@L{p@ͱyIj=0J6pq%fqp0d2|1{w_=عOwѨn6bi8=._hĺ=ZoDAA' [NHn]U$fN4D_<9 0=XJ y9 ɛzPD.<(_$ϱ欿od ?A8O &j9Hd'Ȝ xpk |("jwP BsMڮTs4: ό E~ɰ+ 1YNv󹕖;1ӏ>Ɵ;bj٪;B1; mBԦJtױU/sQ=!_18\/[ysv^eZ*?ötCĪo GYS%g_ ?Zy$BtJ0:pP&>rŵ'p 7~[8kOKw|\w7E`2~_zaEYV؟N_ǰ(P$ -w_9]@#dxpj? 5mCG@M/3F|l$9 V{/;P=Rؔu6قfLcC6ra*Dq٠`TBNd5'J*ƣ|rPu>"- zvQT|~LS\Z?$w TYʩ^ ˦IuU $Ng6+^w+,KG†ML\+UIJu弍/v>g坜;0Z1:0^jʩ!B\ PD-TXdxe-G'(V);>M,/'ː)e+uJp:ph7؎77WB Y&3}Kps^ЍϴE+TpUuC<\?,_Bl;ΒNN庡3 CNK aU\Y#袮uŜ5`% 0yzB7UQnW9;L|yx1}cթSyyH\Uč"?Rnj>a0r"9x}Xt zReK1w L"N>|uc&1(DJRۇuWmaHwOa᫁W}OH鵐)ա_`@qE@/Qk\fU C;hu$ar)夳Z{)wP@_f P=LBe90$UTF#S {Ka$zf _.lV$2 bږTUS:8Q-a{nf?zfUA#!IyIC#o×~cl5j ery.='29_@R7_"=X|]p]HQ5LD7"-8SgAFg:rG+uq[WlMjHӲT"L׏\9*O?UQjÎjp Ou`YmbN.kk.x{mM0+e R6VN-}8-QfXȴrhPlqˀVB3*ܕꀑ z'.߇bp) R&ajUA'w"֕>.ģaZhH`D1j}^Ghw>tW,hӢ\N:A9)&sv4ߴmy*8g?vVg.Aƅ 00c{3k:ܳ^QZs k|ҥJUXK'Toȁ::k|`^{A.23 @L"*@nZW ۱|"tͻ෿w^J(C" ad#F-rF&"nP tn Fu ,œB_}X`nwgLa@ox˥c_&O=ĵe 4i‘mt[yJ(׍!P)iuiɡXTm ΀8J(*5YXu ^?: ɰT- ¬`'~i2dlsޖ!d "v]}ֱ\O*A>-Clei)U"q}q}(N1,o*_s + %ly*yؤSAJ_.M* Rw= (A˨EjPq-תXlUU؞ՠVe;y¥dۼiZ1:{Y{4K/fɴަ7`ya F&kD\ä.`* ;Z}5Uhi1f.,p/:d蓟_<^,#@{>Z;Bn"L8f샎?oU3' \FG aC*UC?( CL'TQҹ;㹿֜dTKBqݨ0 =KKF`")2^#N-wTL]y%i7]ITyQC5 ϽS 74aX&YfvEa(dHW Pد%vctPJ: .AZx9 [L'4Tu=E Ф7Ppr]Rym\yğ[sشO$@ IDATf-7!6*6=Tڂ2RU)YT^sB@GH'KZ {PT=j5 "֝Kysip̜9>vg^(E <LPNJ oA) WϠQiuqEZ+%%WAV4J8 u>;pyW3]2Jo Іe`#Y}up0͞ↇOTM.20R? F:*_漠Kxے0t b"AlA \\Q2x"ʣu)AfJ\#% ܂0mD nC.4-daU5y J!8J̪Tu(^5K:*ڧX0K-I*Kyٞz|P|v @i CcmSZ5ҋpGĴmϯ+P<Z6yI\w$S~UuPNҳ^Lӱ}f|C)_ī|6ۙ: |JA=k`5p1 W'\R5()>BQ/L =3MG>G~'~Ǣ cOAddL'ۿ_XR{p~wzq3I-D\h(%`y%j*u¿xFsfzڱhL˒  'T]r^J;v2b7LS)B}Uy]-\k}((QTY@Qy9൲[r0aZ P#YLAU40!t ei[U e(H L^KA/t꠰m[RJhڴ_^S\]\ U"87 fh[t[[CJA w4:+ Ro_)-9{{Q@l'{㛋"nCV0He-UI@L=~HWLbA'nT67,^f>v搃>$kD'#U"y ;h~| Q^)[LjǐJ;j Nh=? A3FO>Bb!ns{p~02D&,#7]8?he[_ P7u+e^BG;!]_wQm~`sAQ$A9P 7ͷ]WYYyE:$GZO3$`#hYD8Z:(* ! Ahǔ?[uSsk~YO HV{_[q `9 #^J$\FRдqk|:'R hZʖTNQC{Smu%SG/;jh802iE(Wa@ȹ&Wk*i 4dbc0օy~|ED-e,OgDjj7@0-YJHَ7N P))6MھS-4UI1' _Z|#(+9q}vk1!bJn~yg}Sp~F/Uab؀_ }"loTÀǫu113a903pwCA8Iԓ"RxV4 ׮<T>{p@0אg/J2=xz8w~kfj4:Mjrk^)R\ 8]Z|n# 9G:hldZ#Mm}}#9$) C 5c08!8PK^@Ҵ2 ryb$|/P)Q!cMO`P!R{Ekyε\WhG$b*xJi]/VLPԱf[w\iW}7P6Ӆ3gd,qB ,ߗm:!my Naud G<[Q4TGIqxN2ɿ0 Fe=XsF׭a|X2" J -W!lBm4M{{a#'gPf.D[JA%[ը׀ڟ+e!8caSro0ӈ7򾏼p^z3NoAd`G~#nu,Ygh;}'tk! XRw3Pu/vm5 Z&}ʱ eBU,K.|7U%+'unmi[nz%ϣs#]Y8<4$6A{Te؄ZG:AһhֱU p a2# XUYftĴ-%+ B~E/Wm !@5r ER~K+7xU'z#C27A<*Ur3CVL?Mj$uq C=]Q]uH/fZ-"%-P#(ط=rB9U58ҎY,B}!3 VQ&7_SnVN-rݲl Oc/~lGV' DjM9V?RB=FGs ]BᙇPم% p‹y.^rim 08 (ʕ*(X ceO`׮qk=-[062|Ňt1ZM/WHM*J%{h ۶/?lG\Guݴ_xﵰ}Do>"'\E܀e8qЎ~MM0{*A)[yS?|PEu[cOL]ʡI[꛸Q=F[ PXk5I uH+,]M+FjZH% 0hfZ_s C.5<0 ~Y\4f@9׏Dn>'Cr :3lߓRM!Qx j5I04mW$,Qb;vG1lSٚ.Scq@];WzYs` UA(x2E)20r6u޺;/~_/]'k654tr9&WqAUͼHd u< 6F01z-qަ_/K?_ϯ%ɿ( ª^̆LyL0\ׅyf%QVPP˲`Z6nێ?^??bVt6~7D]N 7^iخORk0l?E:=`_q9\ʿw0W_=yf3_(@) sfcЮ3K.V:I/_Aђɾ\K)6B=$`~T;0Ci"^S4yǑPx]'RuY\MH-Q+D@)&ϴz1-$=T+fdYfju] 4IB*<Ӕs k}Z9϶R,[;ӟroPS|,Qz[)!PN95>&cn6Md\u@Egq\KEO>SP܄Eum@xe_/f"L#VM j)Ry MȈ؈[1mٰ,W@1IGwɦ:0U I`>IjR}ftL 3_ lO$ŒAwL̈́?s8m{ndo$@?~cRIG'ՀM#Q\bA\3qKNY-. 8JJ  @_O}=$ ⧷_凘Y:Qрe}DXP! ?1D0݃h 9g YG=T=^Cvcƥ罴:b`͜_z  > 53Q)h?@*Dz.(E_ F`rPnv_wIh0XLTrT=-\\(Ҙ_BD 65P,׋r'Ӷe*/:yةJ_K%>NorM:f!@UF3:ZVs-9D;9l\OHBlyzwyM-A qō=ĦPʓxäC,G-{4-*3>P4QyzmoȺiYJm*mϤD`FlgNN.GRyӶthex*n|*xAg/~UN,},i(İ#`Q ! -߆~<`նpI73KY04cZՕ팾\|[e`އP H?.*Zn)S_A4[p!Peqo(ǟ=``aGט;cdi 6 mr & qYgpء+;2paKw# "$TnE;-EZ}@ ;^w\LB)j*  : #(G5pVtET:]R,m@K; 8j jvjuwkkY [\QP~(t=![QI ,ߕeTb*xDYj艣{*@2}ԦaJ)f)&{NJ,2Z8t akI/l%`fl1yfeԘv^471|N, yT;8-C_s祓ƶrjxfwtC =%T+_#%|hƸV CS#۷[ny#~kqqNш8dա\s\w*/>ch}nH)[sWL sSǞвxx0W6ki ܦ̥G+?_$ 6pހysftws1z{s׿ 0}鵍JZKTAT@)-ԀTݱGFRW\A9d5Qml9 艧@<2V HΡ\.n%V$\Yn;/Vl]lRiF]M3FӫC@$Z}^vȕJjnQzB755?ъ~_LE4,#D*$% 6hOӄWXMgwzAi;dE+KQBT2UhN.d0mK XVf1HT iI~vW@Qt'PTJdتRR)琶'?B2;хXޥ+z+ /^Re>xxR'D_(=D|ªˤĩ=1XLBW\~!~ݛr x߹ޝ?@6xW$Q@S|3O+=[y\@qх5o\ 8#ƶ4>n,E8CqĪXlIw?S,  ~#UX׭hˉׁ|c7`ZяHF$aDãLŜ1 2( :KkՁ0k_ /^J!H*dzSC'2&_i0MDan>ZL'mhORydh-|T^1 ڟ=h{(?k}\Wm*JGÔ:y*D D'{-mV|n֑ihQIGi:23ύ{er!ZM-*-Q/XCLvV@@i6 ˟ZZŎ+5q$)߿r0L xvl[2Ow_Fc?J .Kp_gpdrLԁ4BQtbJ]Ul˺P{@%K  W,2N@P MY)<:9(WӶk)Z%'O(-8ebiT5аڗwn\9/`D5@ ^P1/JU/aV IDAT3Q¹g~8p%SαY?^n9}fq~0டaHba BP[.cfu<?t%V/Z,) ?sM/֓~ `B5F1m펽<՚|}Ƙr/ Bq}(ר)$q yKB%vQ̂OP UF6}La!~wDu0U"aũʬU9r$ Bk{.n8WNK'a"ZuvozSG2Zι15\G;9M&fyùF!jXj~MbjDlL܎: C0=R񙶕Q{rb*>8PvԼW|wgLx|Dn?]ڲj"*AZΔ5׉ ӟ cܬyH352`VI߿(ŚO=&L6sš}k6](U`bb [=醷|@ OjmOP@}90cp^@㠃pˆ rcl(y ~{}J..80 gGt3-~۪ XB?¯0-C2Ўz 9{i OKJFaAS5N8\ڷp|ښIeOv^K5dRfjttկ Io? uXB_ ev㑬k*I?rNLG,irO9V e6DgJ[t'AqW瞹{ϟN?Q-90Q[D \A\3m̚oziU_6I _FJ7%"a#ZzN.\Qg]oАm #Y9%!L7umiň@ O腡dNTc\K`cG"pW=;i!Ƙh+;(/'e;HJV)%sD}`K0[Ďdhw̙K_o&6EY}?ZK% zёQpΑ7Ї|!/ôL`1cځJ sQE+Bi Q3@$^iB=TCv_w屭\ØPOD@MTPDHLfK*U;,QXVV>[I*AOs S [ijZ 좝7I}7Q=K6 0lzؾ *m?T5,ܷ 2< f40|<Ob%%IE89MMxJf AaڎpRsʴtu/&XK'#郃Ȅעz٪بɭV1RՁ\?J;wwNMZMR4ÈU>Rڤԩ-k>7* )d0s癮(jMji.s;Fn;ʗĔQT@pQ- __!7>dڀIr; 0~n='xT^Mx'l ¿2n)[`);e)lnfP9˱xc @PM\ #aV=ShXn8Oc: Wm$w⻐ܺVsYu麒#,ѷLSsluSuXόS!)4I%`-N|7׽}k_ѿƭ|P}a0B!/}:\vx趛1~[G+\r0vZt0v‹>a)ܙ3f k3L0 f.l9z ^̛o~lj2܏LO; ]pN8x Nb9BXqGԓo}7r֮\̟/:YwQPVtƃ Q0Z( ø[P}*H9YبiU]<<Exq+q`Rg*O>]'cmu&)ݒڬM5})M3)@ϰ̌}<NGub"3pdFʮFs(R3zp؋Ϥ8U1Aď3 ?iĝ` iF'WM,H:j.d~lڌΝJjVM~m,zr=2"=-W K2_NYCG{{n>'v " (8[1$_ܞLSHɠEhnu=`&7 Pl go_xz>j110*O`0 A=|`o? 61{`5XT=n^=4N;D`H}>2xT_X, WXjep🟼Wl9-_=$q +^XţKrlN{elظ_>rG+-OQ7P:js2 g v'B 8h~WCζ8+_fʼn<#tė1fY^p814׾cLw/85K,;N{d_uǔ)0w\H9],H)q:΄TSϡ:^`N bZH'inaG Aקi&r-<1!a==XX5wߺ i5fZnM'8}|Kg}8xe״/je8+΂ KB:\~Q0|ڛ|R;Oo7m;']|r vl#[9.L+p @E .x "xr ưt"u8X0bVA}מ[ x? %go]+W"8X!$;J?wGwL`ärIRi񎺐| qJ@}uQ@x1}\H{Y?,2 Z TS"_i]|y" C `-xfڈ6b4XRB0b/̘93`L:z&#VDY/_J> ,Zj)y7O) AJ*jvV @D*EA\3,0L-iw{ 2e:B);9>7 u6 R/-t1m~/M$o)ljIV6ۇSH=&KO\_n I<wdPHVie51ߕK`G]DvGwc՚tݺFVJXzSaV-ϡZPCT=4馎cb.҄WT3-l3m v(`E"T RI%P@׵vaIZm-4gH1sDQH t@N>Ku3 |SW˘򻪬bHb ^ `BRظ,[`0f̾8t0Hi[wa&ڈ Cqh6nوƆ6Tdb؏Y0s`z|ռ X}It΁:g>f,]BbA wTPi[InZNHikI ;o6(πDT'x5g)h3X|;m!,7 E/)> !dPO$Kn 4 wOQF/ -BHb*#jf (aO ^~;tEtA}J#"{,g(@/xu8woNOw0p/ =2QkٳEݨoĆ-(> 7Q#,ːؿ}|?bekaWq+/%$}C-Ǭ"_\ fL^*PxQwV_o~|?zze%#xT?1F T+LXJP9QF n ֍$AIMCC:oZ6v]ԁ_`c-Zx܉s1XxD}J$SwKLcN6D;+nf9WySa,d y$nD~vi)l,`{pВ!+B>Q`wl[c X?)V܂= eZ(C(`W,򼎋eöٲ  E0 kдm ~P)MCh(&ĭ‘Z ┴R3(S `Oқ,N}rع~qʼDr1&5 z+ Kpyul8| +dK#l&.ES\bL/(QXnقh*<u)JA{slVih&*ge"x%`Op9~rY |ԉŋSr У VRlWf zvsD'/}J瓧xUI~M?Jζ&Mk0]i=GFFZCᇮI'U+0sF'Ze,;Gs2j8qϽgc\ 6 \~\&LDw=(.@uo]x'a]Oޗ3l`bFG +'qj|{`l(@1́k[8zsQ\EEay z 3a6ք63NW<; 8XT\ ^|g_Bo|-o\LASM6k>㵸52g X6 9CZ!T>*4! T isPPؾ:rȈi}iV@ % ?^mUsQSְbf\g!%IJUlYbfP%c@a rb@q_qd*Q|$ 9eF("V/{vv0y/bxj#۱a(@ IDAToƺmf:l\GJT91"q`>MO-KNFyصa#'wěodΝBQ´ĵvԈ:GP:U @#o'l @r3A/#ھ.kfۂt 3."=^>;d'(5&Ο˗-K_|ʴؖw~C)'[qLL7v ;/}z'wr,]2338,{Qmx~?ދnCg%pۭ*߿l@ 㙍1.auǍ|~pQ807Vmؼ vlÊK{a}}iR 0p7,M8rŁ8أqqf3lfcQ/.>c:N۴p׿ ==1`G)].8oƙ !`7;0 #@iN#DTF>n*ˑ v]-Q+O ~-Hsu8aT"zm0MPEؚ1aQkLFcfp0޺61!,EzFa(:<&&HjĈT |@oX cH|_FaEYĦB@ZZŤ1R36O ذ}3{ARi5pC~or}}W0LSa~_^2(mdLRiPX==D*Eu~j= @ò~Ab]3-t'i< M!<_塐4H^'1Z HvZ7KG9zNQp_u7ˇx &Foor)@ႱXeM?_??nzȁ_pRg>?uW˟ V}PG8rՍ0 ~n)ʥ7X\u/4x[$zݱc͘50X#gå2߷ , kr@P)ᇷOל"(ޗi#iG(W~SX;𺳏X+sM8hlfG[pIdN?6o}y?p>?ĚW_F|e* 4A\/ï}|?~31x-ޞ.LNpeq>0o/U4+-נp{ qk0ke\KKNcwLq</JbG_UҏE }O߉3pqĸ=r#>wek7O<bV.i<_ u7ካ>~8ת[``;<-@<=/=4|[Q?lA)^=l,FQ166ص_ Bi@eB,7]Oo^w-/?/n`88d\|{?z NR7"3$2ޓ&*DNQ3Y55yϡ:^"jfZ r2O)Z-EZio?r9A# TT2E˨JPvWpo}˥G`dpEL^KߌOp"]j݃io\pp€>ۺs'*&-7 9׾߿5* smGg2stw=ش}papi7Elp9C%bxj|wkރ WklJkկp=r/2-3񕛮E}7k*?@nowL7ȀãX47'ߵ3†GRK ֢:2NkU8Funho wFYš%&$ߛbL ~klb-N%dl;fT_W5 V-V2oaHqM+Bq%0Ll{.R qN@pf= ^&͇z"wzQzN` a>E/_MUm#0\k~Flپ;vŚ;Z0ibtfò`^˝ѓx u0j2u`ydL>f aY݁H cl}"鳵2S%S0=T߽MLHrD亻lV7 `Z @)Q o^DBDl?> _ā2-?)Kxr1N[10ЋWwN?8;\w#6l؊/ťo~zŘ;gfeT&3༎bW]j\"\vL# ,zL{iJƹ"syQpO^`>w5T[~2u[vaXo p1c9Nb&㻿_Ch7c0+.y;N\SLɦWBBB !*Ui"" rEz\DTbA(UHK @zSgfwʙ&Ig39<eW׭<{o:ǯ<"Ȩx`z2\iD]4g}߷o* % }޸\*-ŁH,;yL:ŭ#{U4ssb,}2E\kJlqlw];wNnJdnhji,U Z~,EfQ]2?&hbhEc}B uyD"и>O=v;]Yu9O#m(\T, Ek |mw0NϥaDQ͸m VGJ˶fvL%Y 2PO0h*(q2RA,./tfGi/u; X$;[0u93`^{{=wօ˥RW[)(5k7у u',۹3gYL^I',cԉпԕ1'bM(d9 ȸ|>wP;*AnsoFmi38"nRރ>|kؾ%8(F6JKqi⯮d(o S*TY rFc/rǵⶵE]^WRDYibqR1օ,;jj~R$Ԯ\3q@H.sdʊO%6QxϊrAՌ#S c=0 #:Jv3)"y *Ʉ۱8H2LXx`vpr5p<ƽU fe6Z*WD<.7z&U׳Х3J?VZhi׶MJm.-&%`w0Ŭc.l.9Lwpw0@2j`ʪ̣9'85g=;6[a_ivr:W*Kٓgm}$ٟ`RT85\EM+%,ȮhsPWWNjee-NH[6gfX֨#pi;ϛX+gYdƦ6ּ:7拉 'w$|r&Uc1m@muǮ8Axm6sJ~NJSN:|=Xn* #B"3Aq{\Ygw>Ķͫ`Y x/ 1g:;{.5P$[fMD7n@$di٢V'zAYvt1&iYtuw|U>k.1kzqͫ{՝9(Ȥ ϥidz~A~tY)CFC$o>(8/нu+2a.8@PBe%$r'cuHf 8tZO|h(GM$C2 . cHX#vh~?}cଫ'Jb-ŀmG"J$5WsrFh-Y^d560@)y`I{>_UxHŢ d :l+\@rLNչwP(PaPttwRQ4.˼/9y9ed7ef &6gsU7/o2L^jV=C'p\v9O2 .^۰ C4CERAoU|ő8f<+Xx9ll^Zv,nFSs>h#Z$P*TIW?Υ+W1RGzbFidIr磫r_cOpݯoGdYO}̟9cUT߄;:_ۿWӲTo|⊑,X,A]m}}"=JYZ9ŲJ&8̓ e|.Ϋຫs7=+=!% hku:(9BƜ14Uձ41 cPLEYrdh>ɼ2Nh3R&䊖!GlpFqI(sq,(`Q'JWc+2,<<[4df:Fd46+( N`w56žˎg:ˇK w(XЃY*L?aC!iVZ{i۾N#h.PUVR[UgL21[%b{ؓCQ`.-/'ck2O6NOM+Lq8nhPphP'Ht;OyOv pAᨁnXYGJGuDȲ,#Qٴe^Lk['Dñ+p7fB]5( X&Rhɫ[XWwv4LΨeZr 2LIdB*Ft=J4܎Okzz9|!tgϚBCc+,Q^?Ilx3R@훋ߏe,=|KgUy"%ݥVO鹦ץ(DqLp:ȥ<|i*|NXS/ԣQ[Q;הg·ɚH%\a0U9d KH!"rYe7")|kDLw5&\e:GJvĺ 8{r.@DIdlM,~Y@ 6UUdcI39&}ӡz#[``W~ 2ޞmSϢV`YvD1BIfڻ0 IDATo*wd4k pXw +k/ w:[5sI,z/D##`+ p_~.:T'Wē_5g%L~G;?dÎ J".g@ ɵ0[QH"O]Nr & ]r^}NZVaIE2цacQ6g8(;q |S P1$" 6~`*)1H 4ަB0aOYMss}EdZ\^IQܰkV.uXu;5~ 弙]jw6MɆYF*iyc _/Ph-T<.i&)bq:_aDvͺ֒'eQYT=D`kjrTQ1)afjd:?%.'U# n7PoZs )B*+ya-/< S^Ш{-Z[s+0JJ?΁W, qe*$,T\=+M3d <"ۙ6<<|<_wFSL3S*$>CX ׳ȅ\+2ޯ{V?pm͖]-lmƲtfTQS PKFt!i\Քgkk#}iasxOq9'Gq4ͅeYI4M}C2݁F\$v̞9 Y_RGz#K/G,fMgp tBJі%['S;0LX"/uً.ಳa]4/r9f" yIt6ӎ^ƹ'ϔ漉UĒI~w71}/~b $ M? ~=K̶RZ޹{;$ o~Ww 8趻 ~K8pSt&HR!MY1g NvH<}MΚ}Wn1aҥU[gze Om1 DyKru{srIDY暋Vd$AĴ4X~>hnG0sߝXQxJs;eIe6p= a(n-%ᩂhU D fS.ǣ׭ c#Y Cgo'-mhd{{h@<O;ڍQ#xAܡ2,T4ɱ5j(‚(xsiP`lwĔUGWA ;WD1(MVsl1V0=B'FGY|d3 CM}@kŌ YMHcaYz7uu$&P0x-4\$ 2U]C˲+mL IԼmAUq_,CK?ڊL*$PȊ̞I׮ݎ19.%yU촓S Acg3M]-uFc[3 H .ZoBxBAD4I9J-:gTaݾ$ˎ nh  :ŮIQƔRx5ÑR>4M[uبPNSkhc̑\Pz60z۩XO*`x8ʪg^1c$|&O~V=M)ԩ| '[ٗlz$e5%]ͪ#l,8fb*.9{'VzrfLlDU$yrdSNZwosVgo??1J]-ƎfL_<DSU[U};j|}gUOẕ離dOHmm3(N?8.?,;'D|v$*t|?≧0.MjQYF={psO8<ҸX3:^r~b^XDއD$c1v#7ܮXPD0OCIn4 ܾc;`T@.(q$-PPRv,8F E+DYbɓqtM? aⲾS Xr9 55ʫn5~ut 䜉6DA]>/(%py5HtLFcH;d^\xS@h ,`**5ԖpT"'f6`[.t6A$G|<^PM!PO0C]]ez"A*s UhnN\(ɘ,&f,?jh|d^ |Z6pl6,.-g-rcGK}R`3Ko /K-Oaa:Fg3Փ_V}87|Ȓ _-[ws֩+fIաb[ok;{ˏZXtۇy|?~'.9HoX#;wΛ{4ςDc;~3o42 Yxm}߬ٸ+= NyOh4Z/z Jz.DQo _!=gWs)DZp U o}Wʊ 9Ro)0I"QBy,E,bue9VYpk.9;Ӣ(aC5AAEseyZVԑM`Pl;!ἔڬ\0Q%ە YV]0@E(׮9h#ʹahAehC 0`WW3Ӝ1UE-xjWMjÆSq:zĦ( ӊZhټ0z-J.w00Az<(7 ϋ(I.~ǧN9T@ vvys`0G'!ttƊۛsq)1RUvma@V Gf^yUZlfLA#ÂT"NkuC8O0 xxw!B!? :o  ^ Cϥ~b-?\s9dt.i,Z8wZ@Oe|髷PUY;Md7>$$R 7 ; nEK&r$,?l>L׭{nkhjCBe>q޹s|wwǟ/cuGP]! ތK_BTГj_{/̛ZMM||p獟e('Ϗy1=gwa*BYek[G/&=oax>/8x/"^u!$2bGwcwmϿsYcNrp=QC01+r64BzỶ i Mb.YTB<)w%yLi), ٥p`5[Xԅ)UսEw=.Is: *#h6e|,>̢ خy-b¡ ۍ,cihZ–9s3.w\>_Q s JXlmf{WVnLL1:hGu2"vJkO{:@sW+"]n˫*"Q!eA~;(.%E)v0rXĉz<%/rRY Œ{okg~[)Kp4~bffkjeIy3rM R_AOp{cbMo-U>;?s"G(wƵpmwL΢s?|[rLTEccw^l]w|&V{zɓ8tlmy7kuԗC͇;y* c3^ED(,?cϽ|\K8aA[9>3?c+/3Nձ7eِ(cul;~=={Es)\yQn[˄y,?lR:y%uw{\^~ߗh.*A ŸjJ N8|6Һ*Ƴ{@ϼN`֤Z (j Awu?~5p_TGz yGi"8w [VFlppqhcDHO: ^Oa>`|\ȮK0THcBќ2Q9."B`+TuW^$9ǏogKK/u#|y=:xXb[vld s&WޣUջ'?ˎd0᧟u j˘VYMy~q5WcYh.?pY vzvǤ>F[lmϧ/=;w͡Û9b8}a$"{1L}a֬`ZZ4a)?G(x29 `X$iPjr:z\R܋%fj+K8EQ(Z>(/ˀ *?XU$"C&qc2sðiUT"A=T͘N|hSOa-55cR$]=$`Ի25ɡ Tz"akʌu,dK  74-Uw]|gw-z\ pmP3Q,/"]O> ifi$ D~T2 a L#$@~o0a؈nEQItuC$QB$*ĢdIF%jkY?W$YR0Lۋ,Iw%Q%I%n@ ƫӟ~1GdLo8dI_F)HSIhi}A{ZiN"Beաjb]12Z8BJG 4(#КO >:y P݅HO(Xip$% PUAedŹ[bK IV uǜ1G_6{YVg٬?+S04@gG7 gJ-o Zx_O>Oӧpq8~?d:K#>u70%rgzD;g&fU!ofTWѬho4K߷#Ź3֯oЩ3M2V,7myӧ2ݯQ\_\~i\9e\2/^`wcgcmw0[=a^~t%i!K"w>M aUo3OYwg~T%`k,Y03V(wӶVWri8](`p ?⶯^ݏZzEXx&O@׮G)EDK1/¨\e988Ja-BE&Mp475f `Zvv +R,)JQ& ˜3Pn&:p{'Stnہ ڥF*Pw/;{N$n` #i6uC׉ ӸՂL],m0N1 ;s֠k$ 0  1 u<84GM  5ҿ+(%L&ݚ꒶.*$QF$+ $,#(("/{/#Ni#9(݈o8PjjʫG J{:ኯ# "dp/-=m4uBKw]lٹp)=eYtlJEjR,Obbo]݅4ZW6 IDAT'7PY3dz{qJ9h &+6* HZJLbxԘ(x}5+΀"L\ݿl o le!x686s>HHD`/ 7@ wN 4vm3I̟M]m%/:O\Feee>4 1cO$1m"e&JZJq0u3$6ȊL8z2R>}bҊDoH $ .-Za#a#CL2hdd,Ƈ)dw$lNx/~\G}7ń -]nƝws/!>Wŧ I2͍P͋[j>4eBQdSH:eb]sY?;zRQ=k(*xenY%z8,"'F*t MWpإ9dvkb`99ۥQ[^߄ !'L}GH ,; $/7eÖ7DAtb9 7Қf f;6ӱ07 ,BQQS3WЏSRL,MCRTɪZXrh8?3R9S$ Lp‘nG\3F ?3KmvM̴6`*Xx(PRҥ_ sV 6C.[',ݱ n~$~f9h, ; S@ܘ$S}LR_pgAl/u)<{V^&9#([xY[)s P5dg7n沽m~8Bukxa+L5 U9MH߿UJtuޖmill(D0Ht[<ȬS8t N;ekzĮȀ }+8yJ?ʿҏ%*:`6LZhj)-ܰӿi((++g{Bxpx+kuonaђ*׃"^X:UTg-,3J3gW|W: FYf60tt58LR9!w%'"*s/4i G7^ m*sfsg퟉$f2 vb3뻑7 [?DC/r=9E;K>7|v.뺶qgџGx=:.?l|ո=ɂpcfΞn/̍qK%̛ZM-̜<ثa־ts w>ֺLu~s#gKe֤IHHC /{퀼ע,3\A{.@bJɢߚxٵ46O$(2c$fLqAeei70H__pxQ"w4R:ɤ᧧EL,C'H?!<'`yG;0p,p,FTS`jY7'ndX*A4

Ov]*UED$IBVTE(w oRS }Yzim6jn1Nɪqq, `>koK›\:aO#o),C뱔R>X5 K2UJB,ugTI0Lp?-ݶ9IK 6wиu7M>ixIQjn6 `hPQEuM ,=ca!IyÊL4<i\4?x5$h-pPJke{ݿ l&`FOOff?.@Ohٱ |^BEOħ`{_ˤW=PAnWy3QݿBP'hERHb0k$SLj;mX4ذsDQ1}%.w2 | Ȣ39fb  Ew.g(_lmhd{cЇyus?t.U9{ Ab,xhl#fq1$}b=_q#;`4.eF]ގ(''5> &)Ě8i펼c۞>˲]׈lGH露sVA21]}<*ia¹\rљVb{[ٸi_ʽ?ή]5ͅ&]x]E@! hBEJakQNzS P* 4>M,aXB@\HIS1XWY!3t*XX2Wۺ1I7LVO4Ժ]D2DOHE0t]1 EIR G1 T*ML)EYKTEQLPe5t-J.͚ Z&\.Ym/Џs2n:9͍j$10b\i& K (݆jTW1l$2IHv]TiA*]~wD`dX$Ι 3>8*f@[oYVAdww1 7>}Y= gޤǴ?|Xw. &bO!\+ LeBe-G`B% ԕ1'YʹȮ0zK4^ 3~6ʔj&XKaBU k - $L Y41Wh :volXO10PpQo؎<9 6-Ph/Ħqu#nQc珘;c"_8; &Xdܚ'PP}|eڌTLo*Jq 8ḥx}^4aRgzA;Y8c11$: $Clfdd$g.0W; )ܵ .{J~/}ݱ0};/â99XqO+?ennˍz Wշ%vlx;LKK"n~N]Sŕb 8PMQ٘IZN:'*k+36ȭӛ:v:CIm4m9Z~[f >[K޿V(ɿNS; z#J3oʹk9 z 5$[ԛlY9TWa{ގRrjǝ}R)̳c-y?Km"&ORņ}\0ޟF hdߐOolPq53p1h"cOo,m`ŊjK@זܝ7U>} 4rkSϩl'R."Nl1D޸; si0Z"# &I!hߎ,ZMPh?J7eJ|<>`OO>?)֙9}s|/8+ϊȣK狯>ay>s]A?X!ˣXϦm" v`jI+SYFk7ʄ IDAT驀jeSZ:F csF1Qgbcb|6??Ż!#p~E\ ƪٞW]$\ٔ܀Bc`@Fv6;.CmA8;d("6$Il+921|~If^+$ ^v`LfLqerqsF޿+(o&BATdxRah>"tqbrϢxuZ7Ŋ_((,fѻ}‚65H&m%a&t.1 sv,h3j9E4ø9;vuZ.D-G"tI]/<_M H 8ӺRՙ=Fu5uVu";Ak.Q mRL 2duw:Oق^_P_f&l1QUWEAy1QjPRQDU] vQDJKqiL$ 2(Cƿ%~ CP:LHO 9$JDRB /+c$Vb#cKBo_ү\ǐۉv՟bkk &R#RDzV*Z=I0iOڟ{\%@a%_8*^oÍNEϿ /hdml İIL-$"ڿ[jm])'=%JIP^F0zJ$|]AR7d`4S_߄_/JK*$q.4j uACR(۝AQyJ"RIQL2)Ď\vU[V*xDՅYS&G3fp(a~Mbp!wLV+i1v(z %U2|@&M!Asj XU؛˒~_H0w7a"MwF'&#}ؐAT+S`3JU/"<}S/jx؟O߾P~:;fX@n"~_'y#cX<=9.O`;1@O@ `àBm,1_PQ;ȫEv sؾ/ 6󿍿/;qcHTJy)) ]w R:eʐq2Gm2ΤUܽZ=BPwYL('J"&J(,!좜pbj*hlnj-Vцr|y}),?#j4;"[?%-%ןtPFFIP4:3 b~-DD0/+/ũGR`(5 *bkT)9mՄG&26x0rrfݒoa'c@EqO 82Ne4ͨJkqvA:!U䂱k;Zŧ C㈎FP[  Ě(0~osϪ4$n0ui9xyϝPouTmmPbXINI#Y#/x;^@gդ~^n,s-^GQF=QRvv" "|?{/.~. ߈"ǿtٵV|aG 7/è!\fa"hnf4X7h22zH&A#rD ݬdOFOU C%tۏ}C4ƕ%0ܒ| ˋd1!a~{sAϟ6:Nj n!?[b&6iSNnbpEjLKzL}e7ű2)#?RwCmߚz{-jxdf\xeM@@?6dеo(?q0hxq}Z瓾!O QIRDd3|lVTj-gn@o|&f3lǤ̚:>RKhetpI`M]C-WL f|l8YĽW4iٗ]x`/90lv;$Q^]ͮ|6 ^{cAQłB(,a`a;ЗFQ61&L>]I6@٘u"s&V~ kdaɿpuqWr XZ.>[.?'jYufv;({џ/*u!,;jIEb(#$d\?xLKdBl"P)Ȑ!C!CIXr*Jܝpwvc@hלK6zcyGVQyePR]J씦)vvWK`$zI%Q~*kF3kc (.poխдVXhXesijr5u3iNyMkMQ@aIKI^-Fm7r 3 w/'p5uuok$%e= jZKdr ,^6 |ѨՈȚx%t: y&aߺ$"!RFIG3k,3vfaE,+~L>_CUB O6ulڭyofsnDA unlBEChb*:b%jY$/ArŬsٰqNH'=%J֡ݰk=jQ AA(\7  dlcmz8NLMdx/2!@^nZ !CɭN@T⎧;]MFvf`Q.d (bFm[u }RBo}IM@hJ_]r6_GRQ#;"σSiwF̃vUU%LUI<۰Aǰ=ZmWKbїbt)D#bK؝Phyy>|z=gYf&^}Bߡz08EKq%y#Ccas=ADpTԌz/g\\nI8_͍#GPc4? ErͨT8rԄ憳yIDSe(N&In#2shlnf̻d*~N>4xv wwdD+f:-JM8U6u^^=%SÈKw(4t|\].:'l-Ahh+nfd8+~{^F֠5 g Y'`9P͚XrQg|C/J!Lɐ @2d|tF I""&:r ))gl1GtV?A"Iu26] @Nh7/~ƓzfjO?;6ǃ1U)ӟ+dh%_8X`쁔u]}sB7<)Ie uVf8Gʯ~G]iiNLi2'HP)E$IftwFB$5l5mn@N=9g^03ՅWgeT?&kB|wW=f{3?-e1LMdhdlt[_\:.fݱEBQe C0!6 +C 2Nʻg\ 2dh06!='?yWWKVABqyWs5p;SReD4K+^SD`8{rS&240e$q hco+#@s4':$^ #2+t Ze,|`Ey3jdt۲F}ZVq)(*a#g3=t'U3E 8i\\Jd kWR܌''obl\|P[rTC!ߏ!JJ*+bYG^+2r+QJ6ho?Ic@pbGڵ ÕO~L@o?<\]O솵N`Rfٳ8iy/il2ȶ _RNb|Cڽ7Kfuru3ϿeO%<,DMm=}Ã0]_6=-YqQ>I<Î'4CH>k߿ ʦ |İtfU`L wDz;d~ݼO~]z*~IꩳxnNfٱ?zܝqsr=܁*HSfri(U*ȋ_7I?_ZBJֱȐ!CB(&C!CFngo>֧o")miv=h ѣH' E jA@ď7$IhFLnes%w)D`-{QkyOB~qXCC?gt|. ?{A0ثA<59̪X&|,aXާլf?}syW ğBEF;7M_g/IME<&%ٗu\4!g? AՅHO|(kT헌M? Y(%{g'!Hv7êu8\&?_@,?esf77Z6ykm'N{R[yՇQ*W_,{)*%oki áV.-"`>!wux{?)d䷏CrD Ÿf53y~3Oh6mb6ssyn-(iY>uG2ٲg;kSדL^Y>LMd\L#WCޞʚʺjF({h{! 2dD?Ȑ!_H$N!)-P@l`dz{J},)ml$ %عA W%kX;Pp+{\X<og0ÏVWrWS;F>|* [᳜ :`c߶ Q&( Ya.N_w'/2q6O%DQ:_+'GWs/}@Yy_3׌QSj,=1V J BW'>a{7y9AlKC3t@|Hs_r#x:zu'\R IDAT'roz[7q\]j>,͎|jXJi HnY-䄩VIW'hؔZsj]\;,z*w8艒M cB~.78j.>=uO>z픖UQPXJ}C#$FXX ^nlظ[|1XzSnϨwMz?CQ.3ir:#HCwU(M ڃpէ0hM`|l$[_+m_^ (p2ALz&qh>rK+mngD@/?&&2!.؈\2dȐ!3dP ZMڹ9PxP`y'I}GY3k:yWTw^z l OacyWG)hoYUĊjOםG#;{#ƫ%Ce*+x7u.Kr AQHv [Q( ÁUF+i'&rbwdB\_MNp,u sE>2͝C4fL߯"iFj1=Dlv:hhnFoW< N{$§X7}*J{"p2uJW_q>}%V; \\N5c--C+ IxxS3қ[0Bn4lݻ5Xg<9CDG@ ?pPLc`$}MƤI?!CcLʐ!.*<ȺM̶ۙTZ=3gڕ4C8V3{rk3Q)U悄io5;ID$c/=/$!tXrs|8oW_[R{e.{B?B_@c> 2bf?3squjgXtkl6>| !y+ B:kKܼ@Vb AkX]όiO&3XrST| y 3'1j47dJ@~Y~'.mc-M]ϺXm6G11n,c2dO]eP lA`5z u; ȸ!Iň8N|h'ݛٞFA}=x$8 ݞ'IUuL_p98 =oS̋B(F R@ =q_c9G k'yī?s?.\ءrE]~pλJ4"X,~nz][{dݙ|^{{9>P,C .aOz[W?o'[~5:éU`l nw VÍO>7/г@%H{{}G^(l޳ AP0j0$0>&ށ'4m_Yy `Bl"c ZU2dȐO@&eȐqRܒ|6_ilݻZØA#IŘy>aF$JHٳ{Q^SAp F8<][92~K_ޕ&ARw J߃^"lLݴ[X8.+6[#>55In~2jϙH lluXoiش`CK;ܹsgsqkNNXŘEÄyI"9_85 קecu YLzk>z,7}4[^?-JkL[}~@n-Uj["WQÐ,8&g&}5j6 }gMuYEٸ;Bv |lu`~Y!QI1?+qߙ"3#SO 4x*!5@f b8C̥jۑPB|;bmfؤLLʥ+D&p) b٧_. X8#Ф c'p;z"eC@?'yOy ÊӓXΏpayw~ ۯ(T/AʓMc,]6|hhma֬ +sOE& J8{,䳙ܸ\@ei``)-,w ]}m̭06m]" 9)TR_+eBƧ 6f]- 5)iTZ-`dΞ8{XabIB%Ñzoa0Qeԕ%]ʸ 43h[:JX6$]AdYFW?W\5 P+D$(Q#0'چJ-9 *U105rMxOW#IDHK[Ti@Rg?Li.mu$3Ƿ׮d mZ%|_6D`GNꞹNg&pu~S&XOecNo)C!:0Gлݿr|WO <uv @p{і?{GN]3 * H$&ا=v!" gZ6Ȥħ&4K hҿ)wF<5h u]hN% O'^( RrnIH2<} Hc;ηCݡh]R1sB~<{|¹x s-鏿zMOw[o3~E M۟'%"85dyY~fUlؼ7&0{[hy焖v-b3uk16ǯ+-}qu c;c$ZB.URSЬpeP(T֑^Fbp%.i?GN0XQ>BJ$}PG~w GȹҤzN]Gx  #knÙjo*F epAr>ʕJ⎯EQ"j\Ly%Gqm?l?Mjn .^..#jt.RUZȀ)ikt8'gL@W<`' <&0nڭ44&RUJ5}p\Z4 .6et W$]JٟՁ1/ `ѢYzh%V@qH]p^T\j;ԽNcl"ESWi?ee ϩhN\$(wtm{p@t}drb/&.%scS"nV,n1hOUCjTj)7Lj ML ;BBۖM̩* sNx*khҒPNoÿa[Ʉ(F}?kNcעkg?O~y{:R /AocnqwwwRf <m99,hܶ1vԖחT-譼Z/1A /{ln^!+җm.T)՜;ʞoϒqO_Z8 1*rQ䋠*7gNZxhEO"v0ЀGݛbŴpt`;s1b,oD*15omӆ]?B;ɫ8>ENmXi51)E7}f?b*9y<7k$&ΣW6Zr8y1S Q^C,٧#]ۆ5 Gkǚ  Jvԁn®5@P G?UetS QD_:DN٧#V"b.#y qq"?Nmaej;EL[=a]1ˀ>?FVa{lEW[Yxy'ZZS^^c`r{b>V?OPk|0yQWxnX5?3Kr/C‡͚7`/_՚3ut[Nn+VFe nzJY=J딗"tttuutpuv ( Nڙcb)ue4'Eݿ 93JFNx~%?GZGDGU<;l΂FT2cjgp-3M?",}9Y~aP uw a ğza-2%j @V^z~r5hmŚDJҍ0gsmerf,'c< # MVOضZr|' ܭxn|0} S[-eN-/ ^͖ wl`AZb:<y_yo/|ɗ}4\WZaT<ݟ^$W`x [c,ނr珧OG{Yx*XRzYeE3dd*Jm&|Tjiy[_2 IDAT Q\LMҖ7Ձh4?T.;sxJ_cظRotr,XZۉ`8Y;#3ORoٸ>\(OQy1}>/J+ۖ'ߘDBP$bn% pl ˑx745-#V 7R YrtZ̬&RY#};-cRU]p2>I3mL 2mKω3Ԙ^nz$0XZ 7]@VnW^HQQ55uےEҝZe9d%(}jR DKҤyX`c/DY#3qj%}H$ԯRñgw Ba* ی؟ rb&.-7o^!yMeO:jY&C*W SPVx hNx; @rȢ7^H3DbFŔT 0mOޤP1Ͼ,|F^F{782o ǭZi.$^%1)3 !NnV8gD"F%c3} )sK] Z.Ľ-z2:]samBQi!m峝Lií0\->n-{4ÿ2#f.rOj036Kʊ܂pp%-=t7oy7|PX|ۨG*,5P[g9' '3FpVH$bT*5GRؽ1>ӍĻFMm4}VmFc+|Լbx04~p%mQ nO,*Ft!ƼϫFs032}JRӯKRZ} B|;Ӣ5Zc#z@R t4y Og7Ȥl:c'QZ:{י2rrٳd]v0rOͼ062븹qCfo' tlO̘$acﻠ A.]rNj\ a:^ew[Vn/Lyc&-{QqKV'|NAEBc~ rV؆&zvUK*iX&FXYaia1zz(U*ΞM&+;LH,T6t".VM\7_ÒS#F>$$xstWߣcH>k+(F(Z1 Et/5=v<鴩{+!F>مhZw|_ıoV߷LHСҥ'`еD 'aRȷ<81w_2"KKȲ_j x2wJg?foEw8&> @܌ ~Rok'/f@`5e䟅Cz!f7\lߏCZcnd`fjLvgJر0Ю };$@D l K fƻ hD%zUR/+;'G[@KO^/"~IvV!sC$bB"YJe0bU#.-1棱[|t#o?Ʊ~;HkpoE"lm78]}8? X6}8ktn*ruO"/Zz W蛸>*jj052BT}:n[sw?жq[[A gxmUU_1mnWZ>58ݛb87[gsӑ5բJlOKHthGpG?v逇ʸu>;.|ONbDyFC3(ˇ]A̘0mFGH/cXQ+vlZP3㷱5aK>:-ABWGOGwܽqw&Om,Kb91ḽCې)rSEʪˑeؚ[%ѢA&%N$Dqb$Y9x8= ]v!rr.siN\zn:7 tylաh@ [GdR,OIΙ0`"CukXj?*|W('!~thSCԂ: n;qg6=l fdRCJR gxlu &̤ܱ[skLLy2{]Ճe h,yiOi=HDh8:6m</ ?q#-K֐_XCppkJ1]bPTL<[~܇HgL⹁=5G,6@$kX!?J7^}3iϠ6.b/Q!*ZJRl&)s* #f|R%~I}Lc&\ݵHĄI~9gq CȼHeE9Ff UEeJZuk -{_J 4M0DJ%8_xC ̍8w>?\AO 's2|m#|"[8+m m?.-*|.Ĩl"S)ίם#ЯW(Zc>x:>^cgdBt5ڷ c@rf!$E.& ޵1]Z{ -&+4ȤJ HLL ݃ש6VD"tt0*H+e0el"9._/g&d ͬ(,e{4[*{9I- 8X=:0S ØKōm۸_; :P 4K`= 4x2PS_٫8MTR,! c"A8se88ۉmacn-&60]Q%SW~ȗSrRPVDcϸ~#IJGcoV8>X;ʫ+{8W2ݢ5kHtQ _e|vgqp/6O~!%AF>usʹMGr:U?LM-Me+iQN5!;Z +gŲX 郕#T&ou^|&BrLڠVIYI5}{rx,::lZooOttm04nwfj!+|6AG073۵ ED<⮨'ػ7)&[Vjq#RIM/9s8=y4=unW+oyo`3)BLAEN,]o\i9\K ؿN緟Q#P.c D_W+33gz<1+A)֖P |3 }caR(\mqF,s(Hī 𡘙q4u @?-2X=ayhmG!VΑ&*l04ޔۆ̴B֯ m)5'b%+60i jcδIȠ]Bħ`ژLPɜ9((j5Z P/k`LU8]O{HX6PFscHWw[1~`tt-ȱ.Z̈́ 9ʛjQY6qn,3o6n'7}=o6}u04iN@]u  -z7---d/9 [HFf5_PLuuૃ16gwU~8X糏i`@YE1c_C_ĿguJ8桷\*  LNTJLZMXabdvZP_Յ&_{nڠkQ3L_Y}E% òɝ9lϷXo9vʊj= 9\J߁atkFRƥ|~+9 ޗyC=,; 0NEeSb6^,0M0|FoVR)_jl O_KEŦ:evϦ3)+bֺ\,? ӗrI/GO\[*6mAV@fAqɜI<7G K'FvEf_I oی,VZNJ3|Xjk6g,o}RW/p* /&0NtYY8ckj 9Eay1nm {PZO 4x c rr˨ 3*ϬT hI2 7 {+; t59 4\|JNERLgalJBkD'ǒvA ԯޮ萀:i=ɈS]_˨^ÛF v+tGRVATp=7y.廙k-Ď?AzA&*uzυB]~~)<;->[>vA.W KĔS\RNCTD"WIJO6θYUϭC!WiЪ+MJGQbv2 tmKGrw[zR[sjz{OE%S##tH$hKAOGDDVSS_OIyETV VQTTTWTAW[---N?v^\sZk֮g`DΚ͛C7%YnmJ*k~b/mKfzOG?^:{W ^txumGO>s1b.Tj#ʶ'ӗ/Z,`o䯼OlFJ('"^# wޕ^TDud~ظ'S5_Rй&3tfvl* |8gLT9t$n|/C{$9W ,eٶǼO~Aޭ8[Zp5u5I(%셉!;uy: 7 Ŝݛb9%kKKF<]`od+EX_碥%UseęQ[w 'nuGHAV(pK3sߎJ6sΠw{[>9g)=kDZR KcJ=%G!5" 5RJ_G^ BIkV؃PHDtzħ$p0%L8nm< ,jAΓYsEsdR I15o#'R\" )w`D`(A-~EM%g.HN'FS^]7݂FVh=h@\#*UWQBNa9ŹdQ\QR{K[\mia3.Nsm-m 9S A((&:,pELD`>n9W--'9b/QQ[+~Ky^9Om}-\Z?y6q$cP/gxte͊VƐM7 2)}?1f  4uؘ[cgp¼q<&[tJ/-x)ng²8򩜳^^Ad@T@g}?оWhOS{IOX]gǴ>zY/udHIHp)HDҥT.]#ey~?/kYr..v<31wLd$@\Voill,oL ¬_J J@l=E֠`ҰxdXJdTj﫿Dr[϶HyU5x᯻ZMeu STVNYU5uj t112ɉhR(U*kk05E&kT40k8|n崒dZh1q&_&O.)|8O=B$Dqloj=![Ï9|<%e]3KG+UQQ͸I177e3-bԘwe^\?kCx +맷/+ʹ D#=Hǯ/&ݱ-cah}nDs{ } c3椡۹Ck.%)S՜F?"CÛR{QY[wR][A}\*2bvX̀QUWԕ3~g_mnъixD^I>'9q!+瑈%v{P8]CRjV /DrB$W6$̿3݃sc34@ NhLi0Cu2Jr'=/,2r/)@*a-.6N7Uںb넽-ںrP'u[aI(TJ:t "0Nmݵ%ϙaoiG_':JӟWCvX~X (,%V-jzmL>U^K'}+WnUB!tZPF_Ni)SH͹~o3y6wc;ɡOv5 P^]A}?s5g$o,xODc~CxC"mr!%ND ehI$|f3ɗn0zjt,ոّ7 W^7Gqޛ)'dтp$i i,?Z!ק#8uCvCP")-z8Εq8"QX9ǔ ^coMR߸)W"RĨ bf]o9J%]_yUL]֬۹g' DG[AHʦ2$&Q\^FFؙaajDP/SYUELN^H{2x-]U_ql̍Yq3'eRn˫߰q|ѯ/}CڡRK1gu5_x1Z1`gc{Z.CG4+|$B3߹#wd .~@;ƚCHX[-خDb3QȔXmGPZX>\ KqњppBCsǏETHyܤ2W^kɨXTboǺ|?w6VV FFO#>#_cڠ7'.hb~[k+W 腯m1WkMavp'+% jk-mpO)OHXBiUgQ\MN]G6E%|h?ں|4GʣHd. Xs)0$i*   ;_o~zb$iyܸ/Q{?2>%Os: i7+ѭm=Y NNZO=څ7\ 4x Bn%: fԚ"TrM 9Ց^FnF y%#;gl[w7\m1(5Wq{XodR {F_06<ꪉOI *9ľL݁Nqqx)|#}:Ih_4ktþ bGjV*l[EjN-\ر;̚ ˋ060Pπ :NuiIILXBNIVWbib3i;au] }Wn,?DV!KXg|o.J9ퟡ'u+} w;Ixn\SkiݪMF6_&]JDYFP?DKZ% ?);: fMF,1kJ?Nry,hSWYfV>Gtlm񏼏¢R<;G dԱMίB '#ьziVf#pvc,0'srdW*<$Z8Z% KsRodwl755)[/0IF-4;x:;qb; rlؽql]+DucЗ4P__Jye٥Uh)+T&8ۘaijkl>L&$0==f% {L 9{1sȁJ`' 1&-f%i0>z)dpFYWTQ]]KYY2m+N号D̕'BWkhfl-;z2Oiq F<;#=[=Z%G9;KLXLAmr"'wUU5XXѡakLQT*-WT1vlY̛%u%wgxzyp)_x*1> ;gJ%^OD@ȽW&~2^g_1ԁv-*uiݮ;=ϕH{Zək]/Ε D i̮Z<~wώ{17|`ѓ //ĩh=ê+ >kiH?#9 Cx:)c񧈽r-V:0̿3_BNb%N\h-AGv"v֍g=}Y]W/k 92I1@W[GG&jjpVNYތ|Y2w$=+-5s##C$b1Rt;IW/ZعyÁk"*3 5,͉ ì6^ב'x$} vy9i$HC}g|e={!R) ) *(dž`ް""( =$Ԅ$la! |ys>ovvvf~:g{;~f}o߿{(&Z zh %ե^"UjԑF %.DAYO}m*Y[/UoF"( hNK}ݕ'ϯ5U?mdEXk,Ulپfn7 {kRW:Y;V?FF&QYYC^~ [[-v6(r 9N HS^%e\!"^0WY}'=7 ?QH;׈ o1c0Z6i䢎ڪTs1ҳ-&5Z* `24WR,m#zlKwoɐO0XlP^]àaP z7D#=JXc&r3͔WcAv9LyE5Z_éRviiA <z i<5+n<7NSѨka݉'J?ӽlikh0cV`Q HΗȢY!/MG"("~[e%GFNsrSҢ3:~ӝn>TVpX6I 18:QRZO/'>tO&nu"N@u$J~y;=1OV̹/?|,ONJ@0Wָ1z34ֱ1:yYؙJ6.ƋGFcEIqL1Sl/&۞Mx^$WڒwqwfۇkV[܂W/;_&ܟz_VLcYgܒ|Kt`tz#[,*eG.6c1 a`L?E't ]$D &j)ʭ섑f (/&JW7}]q VD[AanuDnVv~: ;3RIHEo%1<~Q $b:W?,RH1DD"GwN]w Hp]oؿDʐ^DhTq6NIDfcN7Ojx*Vc0m\?msL6YٔTR܀`!Gr%j-.zᇛ+F[_/;-X)U8X#RЪfܙBחزʺjK x:4L),ᅅӷ{o& }ۄh;S\YsmGRud74=HΚ7DP>B\>KcR,iAK?㻥00 c[F|]2O?sFa2,L :3tEqRNԋ{gDT#zJJ+2.*3︡y*I<^Յ&/$X\W*8;bcFVJDB??/|}qrGUKLAa{c-:JV8s?#) R<5ʪXкj vS^ȷ;`ogFjZh5J%8Xqu"Nb4SZ;}96<:ueT}K,5UEd#>`qqܜj r89yZTFכ^nDrK?˯YTK/g-A"iFk7[_0 xѪ3gc&dq`z\ J+Eg@ DhGU7qy;^}Fb21Heڨ(0LXʉ}Ťl>̞]G(F&O2f7\Gyy5<2Ffo۱RQ3wT^P:,݄}ז˯o}צ{o6Os]ᄌh#"o<{c׆4EN֑]*p8'eqdjvΌT0b\Գ?S_ɒ>gUG]qGq 07xfLƢVZ^Dw DQWg;rUҪd6^OmKKav݋L"#1"1IJ[ZuLkgP$F' k؅. 7ݬ \!gwkh|h} fꪛ)=SKj,Q{(=J+9Nnx9㉟n~8BcEJM&$vTivLf>'hi՝5Stޝ{.D?*7\2jkqsvfVVVX3(- w=/#'Kx'0 "01CPXV3_û8EuC !>Atэ n( DYLMC-:Soj&C7܃W;JspwtE!b̡vA))LNe]5iYySZ]Nu} J?rfn$G^ Κ7Wru7-lED̢8EƳ3#}& 8bGmp5Bm10uHu.~zOv)|]bˡ_`Qm(||-a;jCZމȬޠضtѹzûO4nqOcL@pwsf~||-ӧmPBڍ1n [cqa*y:B= oHuA*a6{Estվ{t `@5p ھ6j}δ@l)d6'gOf&~NJjDNo eՍT7j_?+Zh$p:>mA|8ȼƳdC:OϘyKEǎgܢV`h40ML&Jvv689ڡըQ*eR4unR/O7<ݘ0"g®cm< getoeܓ8DR6*ij-PKq䟨&cidSTP9ͦ ~i#9TTQ ؟ΥwBn8f/qYRh6`DM(nn3.w8>Łpw&)/OJk5f+JH=~=SX7IMfZ9U5iEo4`2Ie78T 2&]3GNcs֤7'gti}2q4}_`c(1Q^_ ?lXGvil46o]DnI){h|8dŃ~DF֫b}S[``LRLTFF1n<p3}7SiL4DYU9+bq]*klB^3?gw(vNBE}s^{6s=&ݜ#h:+ g7\{s©;8xY 6:L <%ՎaAaݓ;̮G5 BB"=;}ogg gﳰy<9Nd۠>]ye- T؉.hA FU%&ˈMfߖ,>z*yG;$u<k0iUE.c• "3CBY6J)f?G*\p%Gt''{߽+s:X &E󅛇ӕ?0OoB|-iydH: `B4n 8ZR33ьR goJ&y?LuuOY-3 K?]xIWEw쥤7{cyrCtq1mZ;GߊƦxon+YeYeK/_&8$L2umQʕ8;{S9=NޱgDX@ N1 : ?hZiAD@=H=~o}%y`V:\Mɧ?.@P7'cP7i1 ֨K{uܞ_l4j>xdSgOqt~j/tY6"\!%0s^He'~h(QGybh5r`G6kgᛛ~j@#K0LM"#fRT"AcmcŪ/Sij!Hْc>WQky9|6=\EqZJo\FgͺĆ ,TFhd*s褳_J˶!gب*|2K,ݰ62J"<;%wdR)m"XTs ^co[ke?t ԡ:tr;g/,y/ȀDttJKr$l"(mi)lIQćǒܳ"tr2B6VG&=euRWCQn:PWGMu oO||s!bH≽}9xa49ɶûHH%L6=C}Y{{Q/r=khn`f:tqk1;kY@YNep4u(J|M&Gut/LykjӪ;[ AiuW^ċ?I8kV`eE(nuIP*AȄa }#UM^GIE9 Jդc`x]Έ!}μchA*z/ _{S_\&۟bݾML?xܡ&IaE1mՖe&zvhK9jգTQlܙؤ8Zփ}m?qIDZ"d2Y[jOލ447ϷSyɭDBc] #fjNesHpq pZMl9羘52xiUgేlR3/;uESzVwpǝK;M:HQ dg+qi.hgȽiؽ'Iھojn[ģcm_5+ gGqEά#ʐA}H$WZi4!; GDf.%%4b4 WW'VJ JD&"ZldV5V D.<'}b֟(2 $*'_g(/gNdl"{>|VO&fZsJT" 1(r 2@_!&: ::8ZEH>AP/w +;#HHa8BC.6)f]8nɎ}%8#>Η?USO}ZN/wK6.^F\rCC*yXe*ׅwpz!fVr$@Ks+D@ZLy??.GNI9th9x,#b jsǢ>bø;`ҋ,-h"֢R۰EGt`*`SV][~ßI, z.O[X[2щ;A8#> `Df3ZMf촶߷g=v5_1smx8,=S*Ĉ8#xnc#b$d ) w˥ H $;cSTzh'gxP D'ܳ?QmgЅ.G *u?"rP3PQLqVNײ6$T vh z[܊=vNkE ErvffgF*{ƞщ?.ztZ{,+=æ/+\Ah& KUYiu+S9$l-'ܧpud)Du pE<Ľc 3ز'\t+sfҪ_(G۾Jם[0쁝֖@ PTYœ%V߻/!m 1CEk3."DQ_6u:C+v.56"^ &Y{"!>A{:l4 LRD)y1Wsێimyud/ ^[^DښJ\읱X쭏1{k!|6Qoi;0ql*"}kٌL&Gl=j^|3Cö"L˒@o闕5T Zvy8$2 xQm k#21|q~xz>DG=Ȉ|}KRAD* L|:F糟O_#}7.f#z^X:cFv3,dET8PT993a& |J#"|ݽo~P ݴͻv#bSOqn:ݲ(rS~Į#{xd}"#eR0 QPVg?}ɂ_GtdECs&"*kdIJGLm}v6DL`h#me/8x恈YuWgJ* fĄL&3g_:s?]JaQ~^\TJZ5rZt ΎE"-`Kf=YY%ziQ1,onofUY[~`Jtz/tAFdoKȨ@Arp9T6ҤGHyq-ڥկ#"4ڋ M:rrSKSP]^\ںY[lJo]u-TEu]!}Xw=cCRRTYΫ-0}Wu [ FWgl(*#~In;gŖU4EmY??6e9vVu8׻8RV"" c'.K61u:p0"swC{^C]c=HφJqi((bavW JI c@t":(J #?IuC ){ؖ߼ϓ@PǠ~]1 ]B(3bWvH@@ oD}1T5S_KJjJKjIdx8揿/~gӋ}ݼ࿍3Md&%#m磇$iΌݤC%W֋i{L4n|;|g1xvZ?1#IzZZu4/"H4p^ w=D"PTڎ |W773ߠkmy//m c4n1VC+fQd݉,^Yc NW'wIlS=Oe0(-_tιϖC;xxNIvx_9`2`2:?.dRym*GD@nq3/=bqu@ciK^i~-M]8]Q6Fަv6i:FŠċJ+kqvv@"$dcաL/'X_5*NɛSQQ}Y=XjU89unQ%0aV.ȳi5%e?,onPQQJgLBDDPpL ʨ?Ł$&D#_-JK+hllw7'JJ*ۈFjMM-XkԡRa6[d"/l9_Li^$5VV&Fُ?cKGE F%ŸVq {;fa1\{.FY6= K$F\ xSlF@t{V͙Ҷ5JtϲJ=#OΚ~}P*mEkQٷ7p88xɴFZ[ z^5^73sٺqu &KUhmU.#;Rw_jLf s*IݘɶGxeaC86$_ Ln~aL"o7~wx:qn&#>$- xhkih54%;L)&oy]]=iEDD"Hxt|fq;&hfuo,xn7SE, _S ƃ[ 4.ݰ#1f<{ <:& a6OP$ Rr}ӆȴa7ҪPE;p/Kxf+g1 :AQW G&dlH FGsOǫcomGx{#!/Nrb ]" H,Wr w\mfJ j)>D٩3O2Jj.DS~n>za4v f]STzl?RsZ;#o4E IDAT zGu:ԣIHn G#._R75m'U\ 0i8jyk\(`&ܚ䙗 ?h䮻(.a0 ф\T3ǯihnD~& ~Y“SЍiǙBy}+l^# sinnAP^ d~A `+'?.`sxR! .+(h$IĻi/<5E?!VJͭ-m[aAU}5Y]@or;^.VYuw M8;;i{lzRd6S\RyR*?%,+,ysqZ\E8.as~Cg|2o9btplKtUjI#{r3a2 k5!юiSFmhiѡXڪGE}?S WJNFYk547밳UoE(Ez=]1{&R㖡1|K*FOu W7XZK^JCW7^ ~aTU ?ŖC;hֵ/J? ygGuشo',<>{+ywcqs[9ި'><Gynf3mRWԀ3uFH[36yx ޜ3gtgtp;C{l-(Nizd3R;[-u]&I 4?' G *D@~hzPJs2}T 5ZH$j"BՉoRvj#K pwRo}7$'ꅝ5::;/uol$UWQZ^EʮC|2FZE PM5Z5 KJdF.aLf̢T"!3L{fԓ`_lvF!T(x +eѥ7)jU9Ѧ& vÕ),eʹ\7DLm 'DYKー/POE$BLfr)ع :]+ }cx{ kX646XO;??kS[Z%bɀZD)cim4&?`R~$^_^r>Cr]_~lon4I$Zto+:46/9ːĔVIn5o,<#(+~IJɬ#mǪƣZX7c`CvixC;?fϱ}<2>#+.DHIuF]W+Q)TMFLf֋"4^/y9ml>ȸދ|p;/0pN;'5m'|_LW-ځ1AW47Wn0SGؚފqw!)2!зG촶tEu]A"sTc&4hi7P{RAI幍ͷVR\YD"wW"!tK㋵Yy1HO9Nɟ,fL^vfGn$7Q :MHyMe߉4Jr;kcrd ioY0= a}hySTOlX/[vY4Ι-fGneRm@]c=K/gY3XG\<)>ζDtgpe . tE.NL:{R;~Q zvk>^5BVcZg =0{k;4*5͖?//tz_݂JCCs#G33+*beWƕ|<\*k'r$8ܛo zܽV܎M ',9G F+D"UuW> -ŮVkZZ%Zkń ;=;$:"ϝq<‡x'W/;&ucUPiq 1f=2AvɐuT nΘfhU?o"33|J*ihhBw6BPTJ Ʉ\.C&"K1F EK`A,۴^!lM;Mu}=,Y½7ބ`c֔co%Ҩ׳jgVKNdh䙏?z{n!T1OLw햔 (v(Z\/Lzqn2a4_@I$R$ vٗG),.Opf=t=ѨU(s߯;̫_LGd;]E<˛OLFRGwbiz}_HLgT?HuM# 0z{3hFn2!9p֞і Dc iA~Oedll""f:h,dwp뿾Ԃ+dhdX@Vm[ˌQӮHDț|@yM%+^\)}8fUGDors|mRd<ٓ"/+R02L,Yu+ =ٙ +~Q Jy Δ==wt {xة@\y͓SY>`WH{`Tp].t¿D@k@J[țki(6s*o~IyQ*M~ppY[ =|FA # {\qv~gF*Gb&)2o#&(l[ ge{nrps"1"gFc}C;xf?z6kn6/|{ kfexl2N ABw Y9*IfXdʪyi[jl%Ŀ>44#Ji_TI̞$fH,_zK OLz{?x`2r:?ثiɛ0,`sxF#h$E ?, MB=NG_+T%%1}0KA~--:~բkS_׀hBT`ogF$%ĠjH\_NxWGS_rDLo Lo ${QA]QbE"6D^CIOϜ~DJBS"f3{}^kGs8z]-Z h#{^%cP( o;='$Ns"32F厧{Zp8Ѩ]u|Yxԓf~ԧo.)|0-#%<0b% @AJNq*JZDzqs!3VF=";dC fQ}5\i~% Zb=g3}(ժȼE| lcnuZ"n Xm,[ɂ" J浙# uE;JXYLBzvA.]R||:m*;^s&uVrt1<:-eWo kxa1|rV 82ۀ$x.<6l" imNQ6S5y%~ѧK=G;.T~d^Ҙɔǭto?7f␻myگr(O{ɇ2P/.t1ayv á"'g xQ.Hۘ'^?Tkr`-s1gOtܒ h,. #HLV3[ogծuL[r!tkՑ-;:!QOBCs8{d5NV\˺=Xw3޺1-\}(1ϳڌMm/PF@b;4 U'E3e<ҰoFwޝ!5 F ؤ*(GTwgtJ,\~/;ciN%'k2`Yy[++k7O_a萜IHJtb#`gN6[Gp褲/wN#DŽɏ2ҡ?ZF@~6vϫw=pIhВgH6d%eRC J #⢢Rr 8ԏ'&SfzJB:KM2Vу萕vI:7~"<2eợTRWULBсx:j_ԘVlvZ>s?]nڷ-Oo '*ip\r0O}0 ʊw8pN5*Dxkyh؄K֯; $hN* #*9q`˿ ?1=rFn&>zwd29(^P+>yz|%k x1[xypo,ڴPUWM|X,o|FEM%~>tkݙ=qoHm=%[d3\~wWqwl8ZKtI4F"$vcծuX2LNmrс:NYtJBDL3;gEL]#֪Sw׏+5%sD\]YW^P΁OKuk7(>j1l)ͫK{x˦EbI%%)1$BeȐ@%Qؕy+d l;֑w'JTPeE}!@aY1tJil(8Ut] Z-Sm%:(* 02Æj9ا mvK^Q){%%=YO{tKLV,ڴe[Vcqo,߶ϟfw޼UAb^}h՚bFRMRIT|z (}[8VtLYU F0>zz"r0(=;n7!A}L 0:db݆7+{P<]m05?#q"w(6$IE@N*.:*(  8 V+2IEvo^~ٿRQYBp77 o}@ ^w㴋|u42Daǘ`Y2D?R0^trziZw6wdYuw6É ;+R2(&m*)0:YCeZ3VL-޾?8UzEkWl?|>Y|2VqwkB́ݩT*mSEQDR֏FF$ ).)g?og1f 2ڦhD*' &S"Z+HNEHV{a-:E+A s+[DlҭݛNмu8rs{))qWx!82B)u'lff}_fl[U*xKvflQ?+qVʋyp1ZL=vc P)=*5]U*=;jY]K4 N/ /]EhFƴfUiջIir atzfeծL|$~ff힍4籁3oo8|?ƠՓNaf09uZLJ/7> GPtFvfeJw60"B.(~rV*etڎ_f$*tЂdsA9  0c/xSsSU| ~OelF1kKnNN#C/]!߀.B 9P rq+b\B<2'Fdqĺ,xѽPt ޷:ʫP1[@a)$y$9x-bi@Z$f~pnNq)w#G3^& xHYKXz8jM|7~gtJLg~|z+qcq:E5JN؃ IDAT:MMu-N0ぷz/j'>!:P䍒w>ѝ>|v 5ev;[#͙79RA#C.!Uwe2%06_/xCD6]0c o/ a 9[G7ƦbCD Knq=ӻ&E N)Y6ݫ.n+i|K=hzڌw%bB#D@C0%e:ded53s)Ӟg1 7ˎRea]+=pNO,J""ު3][uD^c?S :8HEAY VO~} [ΌqMk3*(0aXʪ]Jſ|CNQZťҫm7ڷGj5 |DW- q<93oS]m)*/a #gU;asmZJ(x)(+"< #{ @ʟ;ֲqF\C>Mo }2ztل\&C$b#I;so7/&OĖY{u:"&צ0ZLg,~=IIbh$F6byh2X Ƽq=v}snIƥ{*6oM,"P(dĄEuZB\EJ+Sq":+~>#'x~A~mov:BCjR\PŮs ݸ{y15ux.~?vmgŁU(Y0m}2kϖ\C< .vdo~><=dn~Z$a٩3_XJΑ\`ڭ-hhۜNW% Iv'M6gCw?8? Ar8Kמx5т/Ϛ#%!,=:PuHQoidqŕeغ|IdnVf%T!%5:"Xs-3oLV3UUdѓys31O_b)6?aMrRU[aëARccƥ|{ KlKĶ=ngֲӶ_S?78Z5 :lOi)q)zxMVyumr`;ɉiud;Ğ #'ӌJBBȱtNm܊ aҸy裧׸.~rZ6B(M{O/TJmZ$Q t,^3_ Eκ=n,W ;iP7VK/󶡼4v5V泏^/ShסHOa]73ә|2p"&ڋ#@f~{:+~%Cr)ħciA~zu8 탧 i _RBp(t*-yjO_Gي$ϗft*8ŏ˰Xlv'3e_=z ȑ2q'JXl뜢`m&rY  B.sN%%Gֻuxn6:ELl6̈1ם+\E@O-N\g'OPeemĢPəd58q":mn~c Kwlh࣡u|$؁\~qBd5[P|(΢xZǃo9HAa zүwG|f w-l߹rdke8 9CNk0vCA$TwwYJo}r&{ `#{bja^2SZ! < :>i#̤&Q(UapgH^$3F)Ģ$wrq8?m`ĄDȇ8{>SCLp$eK/-҂F# % $F&{)*/ӁDqmy/}uE5/3ŅÝ}F0g+"NjA넖,Wt!ZK6]٦ ]Gd xc" ^֪)1I7:o@_ $ɕE$.4Q-+QFHфdh6c4ԙL&3uԚfLF3f É.t@o*5JdQ5<^xj1ܴ Ƈ8ofӾm :_ӗ:sHneߓ؆RG((%/Ajc6K_IBx,chqcFf%< {މjp00wYr_VHo0Jb$IbD <²rDI1OaWn<B.穑Ȕgo,W@?RRZΛ~ֈl݀7.q@\C[ ^g* FZf,IYEEiA5^~|)ʫdWٴFLJ&EANZDD0%%eXmv4琝G,>8팯> g]2x8g$gDimTV} !YwB!x9ۏmfTXO*윳(Ov;A~d LFuu-'9GX|CI ֠Q]Jg$lv'&Z3*8YZDzEbRR!G& b݉(xtkOkٲ NQ~ YfQY[KA)Ar 8MXH Hm/{%IbϾ+{1nJX16!DxJâYXb =K3Xuο#[vкselbַ^9rq|Eaq³x2iCڠUj2aCsTU%)$b}9EY޼OhAEmwoavs%F&P\^JlHAMPW]B5"KDְJM 2B|d>7KcOeа ZD'2i.)"J"Ex2Ҫ5y٪S#[st._<:'w-eT,]~.ZgMDܹA to /ysʴw7tNAd}pQYkNiVRc[U5NOd_qkt$ŒSp1m|tk7Pg6E7BjqM Xa.%vowӹs#20mc8µbj~\+rAY}umjr ĆF@h4t%ڵ˿癩DtJj" 휢id5Sg2b33aWRm^KZQ2MBňhla1َ!^@e Z ^ΠFFBBFWsSsSWWѩhJ Bn7pp})7+vhS*tkՑ?g _qIEM|~![Ƿ$:$}G7>;svê_xh !Q| k3Q6?R}L}NQ}r3BIK!>,* FNmrJ)rs ?Irw;x,,f.Gg7tjY}Io+rF77yE1kHMHDbw0_<Zi~=Û_ĠmHer޸>;D\&j7%." Cfk0N2(͢Ky|O(_Ƃq()>EItJCr16mL@!7Ib䘧ٳ/߮U#oFμb?IfDEȘ U4jOug7m&1x@g>-u裮XfE A(.)j >ek "]$nJۿDP9S(BJW)c>\}y @.F4oZBB 1U3cidVܼ" hIK7RUUjfp8uN;z^7=y뎗/oDHB6$TVNIB#Ԏмo v#ذq+lh}d{gz l[Cd?kX>w7?[fը|*kQ*{(*)cF\˵n^RBL"Eyi!$ʇNqly8Zdf\.N;ļO̔ZjA @Η瑕?WoW?lKr^ RÂ7&|6|]=mTJD.fofq{}+۳}Yz _`:f]ۂĈޜ2%#۶2}L$Nf;qʿPenqg3Dщ$\R:C!huJASH9_O=:7oWAMӹN"\$ BL.㟚27fI #Dۖ0(_Ծ};-bXAo?U7Kׯd6HFJjyz7~;%~1lIE20›[1+w%:8f5n29C;k׭;~ER]WnXbB.h $:(8QwoM;fq@{;qw/*jh4Y%s%& &Tay@>=~BLV3nZ7|ܼyy3mOr7naGyjc 3gu$ofOʋ4y#1[-zQkd3ExP' Ӑ=W<@!Wֽ/1ّ,ڰ6ptܡkv'qx'P4^3fП$i qJΪըǛ扤$\5xRx{yг{~M"fEvѾe N6$s}:~} OrpA2&QAۂhG2*o]ͧ3gѨHKkFVVD]zZE|\q_EQja25Q[glPU]Kn^!v|N'z8I:& >^seVXa';vp?RF3xi ] $ CrKԿ gdfEl۹!+fH ,&NoC-=IJGwrLsw߽I >{lʷ%ɿ3{h"i2u8xThD֮#_jv\h v!m۴ "uZ ?2gP.0G;~EKaww栺NuFرxQH|h&_~;.Ez>:cЦ-?3560e3c|M=5Z]B͸~È o]$9afu>8 d_\"#/VI)%<4m,fQ/,%@BǸw4JTVWB@!Wp0p=ewn<ܱY}!:A2kL4,iML^)n<"9%'ix((=/Lz#azp'?K/ kZG:dӂ &^Wz7zwt3g+wӽuzFdN'Z/9yp(@K $ jQ'VSVEZں:jFF=sΝdH}ڬzو;A8TRPhr49j 05z/@n*Wޕ>+]ifnjjEڝQqk pY|kӫpkFG ޛ }3{u6 }IuXc+!6)},>$I޵_փ;inOÇ>/la|=|ޕPVl_ͽ`VEl=s Ӊow/trS~d.cf݄׷)J"ri+(8UHԬ&)S1J|j?O9"kǖ? $IՖWrY}xGL|m m IqWE]ZxM,+EI/5NyO@Fic?G?~$L5x`(8C9'8pvbާi}/G个{;qT9OR+)^$bw[SΤ]No a!$6!**AkL@Qը/}ecE{DQ={sXn;[w%*:VqLw ! OhB[Ҩs;w_Ͽ;v{ nǃ/ A#QVkUyoj:9v3WD\r0mxgઝ}J4[E@2㵼4mU;,-;7iHNr"Ͼ!-^ͣθ.Y+@.qA̚O>:h@lxڟ&}ʱ[ Qa6P3)Us絛+wEN\{Mb߉Wo`ƢyؓɏB~I1/w~\Cr?S ua92A͜ٸ4};%=U5C]gxyב;_{adZ6oa%>"7s`mۘTܴ23I}]WXõG)8Ϋ߼M̞|g]8rk):d(bMڸi~yWrRܾ^~4Fj:$g!9}[XOVgcֲmޚ=Ѧ ~)HoFz4NjrY}5EyOΤ[NtnG{^#N)8bZj=1l5{k=kMFjMuYԙ\Qxj,Ԛ]QyÁ(Jd JZppk\+yӫUh;ljR@#d73o7p&h ӗ̼d?R+=ofElց1բ+.y u$(0`iC-{v]ӱ#Lj > 6vfHܸH |o{~s*]:"dj ԰b?ٕ~S sjuU?q0n2ji:c{@nd$h1QQ[EBXoGrJRun}9wR.{\e :\1~맧ixx~0؈p6g?}}C擟1O|pZ4ntoՅ\s<7-孙愗8<>޼0)^ju ŌI_/ODB\d"(ץcoـLMN@aX ?.r4vYved6oCAA)% hsɿ˩ܾ]KFEG,6W_)Q5JFuU "uoT+¢2Z$ڌ ȇSy隣,'286bYNߖ~ed/nnz2Ӷki+YjS}L2M2vc-8enCH hSKiJRF(ZܽHpD{3@ ;;3=\9ن4]suj3//3++Wop5&+WINIzoNqAԵ$(H/#-+w8>A_gCu@ZB㣱0W $DtR/sƬ[qY٤M#3Bpf~۩pFt|&j^X30oFҧeڅ!ϩkhh}i/yP~FDv}7Sey恍px+)inv/'Ҳ+d@Jf:%;W"O5D]?b̐߶$q'='dY:Q@O¼ MTbfdUUJݛubߙC:U(Dr2z?WZ')Е]s%do]$zb_o\N{ _-WN_T(Ujkm{!kY4`(yndMFfiTr83df-P"^̿X6נ*S{ϑS:]:Fr31Uv0pȧZ戙 6`eiB!%X[ccm} OT04ЧDFu=/BCս-|1u8B%p bp4RD:,r P'IOƹpuuHw#zlLM)%lqٺ2r=?}{g_jtDt T=k3ݘYfIp7䊪Ϲ5HrJ*Ʌ$*L iUӊ!mwR, oW7 ,u:o*ӇcH\o%qN ifNNn>f@.m1!*# J8*wŢxEî.LIt V*;dWc).(LT,-рR&8m5yӄєw1ms<:|iҀ3gr.D%6PQ/<2.0ۏ{3]?Q+;GֵvSP*xgk\E^̜))=5[lRhJL |G_k͝ٴA!ѯCwݟarEEfѓZҥӃ(M2"9'cA0`X[ز:6j2: IS=5O!.1ݛwbž5kK3svrV2fo\Atk1hB ^&ܻۖ1o/5"sa>{SvmK9w?OPmsWNSX\s0}2RǣOSTZZR&\VCIY E%EhtZ\hY9TSN6 lחWϡ Gqb/7։ [ӦA }Ck%calNψ.BIY 'ey fF ]X+=jE=xhD-:QtM*ɅIɼ͔[ʌ^jD:=L-PNZ5zJr2- -+TbZ*5j}=52C }&:u1rII,Bۈz#Pޘtq*dh҂@?~߸C?}!ifD ێ%5+7:Vޑy4jL&Ђ[dl/~$IBOWݶ{99ǧ߯##d8XU=ûa|1x|Knl\ME:hro֨FEX’"ޞ1#}C޼$"IR'XYqQ'WGV9vLNdBiS1dDqY1gbcoİ΃h]DxP&f(r$Hmg.Z>E%ETdj#yBg@>|+X~ܗg)>qzF&_GqIXY>LId!Wc*ZG3]Ow7G>x8GĩK]O$%57HEjjEňby-Iu pw5*k TJeIxZ< wHH[N ! t"7o%U+ wS*2_fNa4{#˰~rn*jL&Fz*FFΕ(©tƫ}.DOҀOE{\I ^''7C\qtǑ-5\N@aIa>!ݷ?m%yElљ>.-(#MxPx[F^A>er/I\ r }#S%Fع܌pdžXaifuzCB$QT?{XŅ!% o%ȑ*ʑ 7 |D\\4YOȿ:paf7'^l::$q!6mYs%_& 5;d)x^;_gl&[̩|wB]*p5|S+;DpJ`.e۸߮X|LؾysIQVPMfV%%B9yzk ep7) Q)+ӠPkQV!݇FPlz;Sύ6pck&NF ˝桕RK4XnOqVpb2}񅏕@ IDATҀO:Jfb!&|b >߆BU\kXY1\ڍ{_غѰ'a-pB.,lE`J)F ?iu M+&?LRJB@c P)׏*!dA¢b2HJGnn>VVzW߅5JFVLOWaُ­ty0C%Ξ+DvLH̚D#:j7|+06q1 GDr=ՃPHW ݃2$70 K P %hڧcZз^+f曮AYsӾ'}*?OLehZ|>vW681 ߃aG%DIK2V;J>N9&Fca{ۄF5[7`[tyvc}\cn]z#1zŽ#tE %5'/Nw&`v׭<ӧF8^KWsFF|g7.Ì6VfAq!Ma|Ո|1c~߸݆p."O`E,(.$ RoIY Ro^"8ؘ[ABJ"{NԂgs> _z/zj~3)iL?JHMzy׾(J4Z ť%DoVXGW<0N{zߘ;g_jJ9{C|Z&/3&4 j,erB ،vRXRĦQYH 6RcnDc+[̬ 04C?\U>>\ua{e#;O{j{?eY(֔W{fag.M_(Y{a;r9c=Z|gH i^P)ɣԿ!gc/YYgES)cڲ|]͜\f=}?oҎ3h/GQ+h֒zN0+n^Bjf:P] PFڪo~nϋJql|f$r$Ul<  tk3C~Y?/pw`߉( Jܒ' I7kU L[ZU}tû a%tiҁ^AFSHtiÉHTp<//WNDC|&ƌ\!K&̮UZ'GhZy`lj=ޖWN8 = k3KSovZ}N 8F {K[O]cEI$37DHLay}}grV{NX06dl,ھGM,0fL!I)s.eщ:7$~3Z 5+#t=t &\o::, |'$3^{l={jV##7{%$ǟP(4iژZccbXmh/,)絿3g܏5:6nKJf*{OTJ`|r"0!lS{QRV\A!\qZ`mOc0f7&ytmَ#gO!㫷>ʞ]tdVM^ThT+y.kmzt/Lc7++)!+J|Gt8лn8uS ##Դ lέ!o)k4INXY׸п4yyWxjDJK(*.!/7r/#NJ]9ýT$IDdڑK桴ꭸtlam2k96MFAx#뉹}CB>6=IE휗:7JJٴG"3R>u9_m/$v|L޿Y&pen.Wύf(N,YI)г3k@ܺ>V&/DDXy_k|sW8Wyj& ] ׸ wE|akhI;Ty*-#>S2ؼf|(ajb#>x;しKGѧg;f]+{Q'q3R?:{d[nL-wBɹ ,'_չHWNQa<`y> BIL[{|\= @==fԫszGL6D[W1’bB*Ey\IEPZ COǑ=6Ps<ߏʍ o=O'93 ,MY}))L!~LJb܎cBӀFՎsbǕc牽MJznf? sc3uDܺN# 6I⯐;^N[J9:xM\)ȥ;s)*""=]Ѽ3(dgoVC<= defagQ{;HURM).qV,NKܽ7OǮ-nu`[ú2}zuv`hL_ k.~1066Qh{.p+j5Y9($?D1Q h/'=zYwo}Ϯ%ƠEḽ xrQϩDJ ()+ VSSͭ;x:&47&ΟƗC>$=k8{`2^mݣw%1iKg0Kw,HIĔݧ;d.D06g);yٌ33g9O|jz=Gh*cgUjʹ?ӯ"/nxāK~A!F7VEQG& 8v<}z>Tl}Xn&;Z!ΎxҤqvh4ZJKJpwṳR$N$3+KAwHMGnnss(.)EQ\RNp$AHzj~F`Xt?[si޷R[]:ΆGJt/3oy1G[^iߜ-P?Ȼ<:~SMtR$/ԙ,qV1ԓRiޗ}IߏоpunI66CPB'$VD<ql_~,F 8;ّAǮ`UgN'"ȄJK)c\8OۯQR-M ~n]YPc*-ƒ,d=KF?3x6/,\p<=ygt^591nhJ>t!۾5=v"CC}griV,9_Ό_eEW|h7A&ӊ'OV|jwoŘbbTԇ_{oGϊw}7̧EpS~Uݧ3qЇhtZfO_+g2o~SҲҙiSߜXpcl{kb9v$oƎ`m 54UJM KEf%&N+\qkؘ1?ӛq020$;O(ɀHLeDMDp|Y{p#fFtiڡo܈lrFtZi|JJ(,)Tvg[."7Pmkmz&>RI at򌓮^U;{ b́ 4fT75DQϕҳ|]>A& 4/'y|n|Gֽ޼SE{9cnb+~3L}s"}>1&S4M1a >3c}&``)_du#-GXn>.'\%ǎ&kvcߙC\cߙCZ"h@o5?5 E# bw:’"̌LFUvSTŘwV2k/!B2Nt+SKZ5hQ)Ƒ̇\Wy[/xwj$VBIJ^N!^SK&ᇝ 9IhuAچίs#)?6/ڑ;wa$v}076cw?<܈Gv~.nq,$IY}9VfV9G#ǐL:s&ѹI*hGw_Ku},YVNdG_AƏwt:mZkϑJ`'}|\:[bo+йX6lw_㑱׸=}/\1j##PP[SjhFFXghyZΪMĬɒA&ʎEaHzO& +y-38 .߄ҥcatЂf.N|./XclOA^1 :\$I@"Yk܅1hKh!p?9&F>9$ƥs`%>|&jcFcܺ%$'fg[*b9=tht:?D_awӸaPE$F8ġgH˸G7|&5LWf/Sط It$tr7JTJ_{Hv͙>7|UTx=a^.|:7̀!]魻vzA"[ bq\>r\4oKc94=?5M4e:FzslUڷmZmGZ-9XSD?-aݦ݌ ᭽Q_gXv;o gst$~;6CJtd;Qѧ>rJ+p2Rd,߳ Q;:?ͧ_nT;Q$_+{dGP # :i_ϝ/ΣWNz2G?"tJYL-pr員q`MtnڡZ;IyWNJjfrGQ+ #)7kdPI8#'EۗvjsF4"qqc]$g0ug_NVfݡM-2p :Ff+k%rU>#9{u{>~Ўx$qP)UjJwHf^s6- 8 5jnbU3C듓)z.q7nu0g܏/ÁsY>ǿ؟?}WD:ԡIfcGݘ<;M[KCc;V!-*k;vEbp]?mrw{OI3z ;ogg\&G'*REDIxgZtVE4 Ly8LF^Q>K&k%։WNU:oJ 88VK,W}>Mflg+m(QTSZnAv䛷'1~$\lQIjPHߐ;FB-A#Py{AfЧ&<5Y:7i߇0m |kv+_DO_L[8(W+O$DJoII)(E: (),Z Rf#x$M`lR=  5J%5J{ljUdvxol32S}{P(4Z_sG_ pB# $<{;+L PUl;rV aUܖs| #PGz69C>ֱrvtdaW|̬϶Ӊ5v#,ғfgfLQQZEad%.֘ ДjQT4Ą\ߨ\9qMC̊[ٶƆ44GRNdh5 B;b J)\sw/ХcNy/O'Skws.'h=:; ݺMPX E+d݂'mq,qyv2Lңo_G-dp=r]10~9hQA)'\e~~ ui\ufY>4lt"XMwZЦW0'шY6*cȀִS[1EQrWȕZVr3¶L&3# 8Gw_T@ND'4:^UAzm0fNcx?_9Ev~Λ;\EܦNs#Bymhe3o޿{w&"J$Al-p6ue0ڶ_,'kZFҩq;ḽQ*D߼gfBZ~ y<,͈sWO3gBLqs>#+ $jL3Wo?8m[120$9; x9>YAil4[Ax:E6ƀ}kׯLH߰uZTmdcu0ӧ5u 8HwP:U}6)ymd_9񙊽]&-A)8~,~ k]yPP\mː =z=kxC 灑!%p;.U{+iǔ6U/2nUdHJ٘[FX{hwӓ076cʰȇ|oƐlFE<wk%$' f,8K(Z6lBj g}=5v6o؊c}ur1s$~3VM^Ċ=k9r)}C+҅CY/y{nvOwA` 27,< [48y;qF|q&x+ OQ<PݿR+ȬgNd"QaCM&Ȟ> vXv5AΔftnt!{DSe(n]OUD31sT!` hʋ&nلn]Z*2BڵxNDfV IDAT6FF̍GďTtiђx*N=9loC%3rA|ո?»E d{PgDpyFc˲S̙pW"U)E0]NJpt_4:5D9eDOd#JEE%dƆo#з#R3.{j!AYQ~Z5q}bA_ݭFJ|N+r`[Ȝ}qsF+T޺$|#MƒavI2T۰\ޚvptpw\jIZƹug|cR^^X;M^8 V9+n01|3wZZ WiLqzS ߮'Pjeiƌo1`'deۛyމ?V/^!HجXv'ˑKQJᮐ)Ȏ{160EP %}%ә3ǧmz9zIXwcoiKv~}[ӡvҲyHm2m-lrȧ$e0oڂ9rc=_9Edl;W[mhdd GӲ{vV7hؘVRTXF37d^$[qi }f-Xuo<IYRV~{sD9 qs9*Wn^#>) 7.SP\@}DQb {'݇r^2GȮ4~j+hƺ䕧yXYpMRlHZVz|#r ^&J",%*>[Ve8Z/(rVO^}"}CYseDqmI?0i>$ ZgC:O@`>1m߭~b};Rńo٢s-2l".6Έ:'kAv%AR} 驅%I,lsWaIIR8w^RLͤcwG۲{}2nsGnC#A=?wȄ{8ZWxWcqԖktl '[{r9ϞKUBD XIIY jB3S*6bI2@۝ѱcdȪ@obboV!4Zm|L Aej/3sp ZM⋽=+VxjE!>xkk'ף10ï-i% ]V֊%12ԧOv>/A:LAB&o@n&gͦ9r3F4;[RS`SVX˗ٽ' ZGrG: J0Va*}p;xdɉ} N2w."=iٟ&n評ϱÙwfڴ gƹYWnZmy֜\$p9:["zuyu|#<(n Gm.?&k`fek|~},9Ӎ#⮥jn8+Kדz:_9]Z?S4Xӆ ׳{> 7lWypXbM"+(2 L.t?P8{#sĕ34 ]kKЖ|݆6da^Kzvxsc3Bկg}\t)53vaxǛ?~ Y({Yi4 G!ӳijɾoQ~ ,9r) \LL8 ADd`]~Qq6"Q'}>=sG\Maי}9;p'5O^ؘYs:~ڀq!*bDӀp>_ ͵3't;kHLcdaO%-M~i&Pqi /!>U-ɦi/iߐ<]Y nϜvAM\/F&Fԉhf"P:D`3;"2ky7j͢˙undIK.AP>8yӹSQ۪M[oN^Q~!`au\q;7q&;]հN؃WcYuZQ Q{m Z"zODKkQRVJ]Ը]7B_r&<~R1V[0v+WJs}[zuz)H݊aQѠRh҂#&,]'qVZ`+hKiY)}A'd$݆&VVRwW cU"pr zzzKD's#&&]:FVs龣 &- \sSarC๢['3ѨI73p,g]̛ 'O++ {vJFٲr\.ކ̬ұ-;t L1J݇Gg̰>l;p?ͩ+qiF-3~*C},!<Yx?27| УkkJ $bۮX1VGy˰g)fѴ܉& ˑ˩ 2 qrCwRXv'3l΂;ҜMUOB#<_輔5GaO?䎱1wD54qWma <6̘4Ni C.3Pbic Ex:m}`˼kxswr̪#ۮ%ैqMF zk C:@)ӗA݅NM)}|C9s=s7/YҴ_Ϣ˟JݲwzO1Ĵ;x9zнEg>3bnƅ1$l 01ճrl'ڿJ~Qe2t|s[&G@('ӑ$LLp^%GS@xTZVZaVC4ƑOM_}?::;wO$R"SPJ)Z] w; $#6@Do}b-`f=s}>ϳ`VԌtR)/ŸsiZTݠ~L2ȱ+s9RIV_tZܴOiP6c;b j ;Nar߱EgT(㊵,ܽ=F|х._1B`jhBRZ2;ښ"*yDWNs֕|&vf&ukҥA{ӨTΜ{_iiL^ 굦C߉d%֏u˕(%K CǶA*ъz_wl[BdkN>ϨJerT,ܳ[O23c3FtUY B g){ £|%*;{pQX˴o&Pޮ,jcN_4oї1e,v\*g9H,Cf`ibK9w2WݠW"`eY[u~Cח-2"*w.ҳI,M |U)ͪ7B |o5!5!|re(kYW =|5d" = __T"eǩxxح| -k7xFۏەԢE.1p~ݺ. &ŀ߆#KKht\ٺo]ESMШ[ sΖ8:[ҺOu2ܿ{WB< R}>ͻrxr8RߛSVL\!hT)(P)sLJhޤ6"bغ'ūKK\.8Y`Ѷ'v/X3,4腞5/.iaIn_ϏreГw0ނFkAz7푭U>4.|T m]gȂ4onN.Dƿ.Ҥ'Ԫ<9[!\7f`&aroC=6ɂ]42ә9p Mbjd\*Y9Ւ?Nq}~Z7POϡ)Q GCn)gqfgrvΤg. f7U|y!kq'>~ʿsE"_3z7sCmރ;)óxz­wܾOZMjz~~q{K|{+;RxEj;8r Q6\=Sq-}je>>n+5f]u^ +\2'^;~d̬cR"`)8V|/.2s e6xL/ XLBr"zr9.\~x5I*Efdg^Ml*_ǫpƯJ=8x( s]?EjҠ=0z鏌4*;{b pU8H$&~Z =t,J'~Te﹃8Pv^ײ>Z7~Ϭ?}t֨i[%n`穽||Y5ƵSۧ*R z`gI$T*͞G÷ j+#eENK!= XhdeB\tf"]~Irw}]oJ~>s UԤ|2j6qfwRxr'<VKQ4Ad)j5"H̉ >pdfMeR$Rs M EBFB@DBQyӌ9;RmAĵTV8F8t! MHWZuO#VȀ jgοINN܄J]hӲ>Zp16 'tDtIs& ] 2I)?xv2ڶjH@]?~Iݶ1>gH 6,D؋TܻŲk-ހvk @ƃlY7$/p"BIziuu*8!:Q \X`5-(֨$ɖwC:{T pi ZJdRff6tN\$.6立̗ui-{E׺,ϰ70F&*u>C Eǀz÷(6)S3r}3v6561ߤBI[O0]"㥎m8{"B3P޾ȓgф$O\nH%R3sm janlFGFvLW/v9^:Iv嘶WVdJUgemٴ ŸRI O_R4Z Z}UnI7 ~w٤3\NM˞:݉ ^PkԨj) v-u[l;'aݴ+d5X&P*kW `hZ>0z {G/0188$%c8f& cV}k1wյ\176yD'W`=aA&;ʭ`)qmK:d#i9+Z.S'/D2R%%:i@>̎ndEFqYI XdžuJS2"<MYi?Ip ZKbфC E'%[8x,[mK(j2RHIHҹv@JB&n.eEFM1 ؉KL_עU?/=?W^ĵu3jG(F]ͅk#:J>c9L4__sAivEs ֭G[I)deg"4+'p|kFըZCGr>< 60޶?y`njNn|~j16'yK+J XB{"hu\RˍguVHFSDOЩ~[zDYwNm9]1kݬ^}n>WE=f}|ʲ12oC9d4&q?ń^gL-054ỶߐΚ7sFw*];`G3p060"5# J`Jȷ;OeEyfWoߣqz7&9= RƉ7Qr|.IHշ]XLDFo);ǖ;Рص%vؘ[56[r=/BYk/_Q۫Neɘ0BZ-J ߔl&q%ݻLBJ-j4aUZ-Ű.ٌ<}"o[b]یL~\3mj(tů[q?>RR?}+1v|+y]z6Y?qg/Yf=#G֓{L71OGDbPdЬ^]]BT\ ϞW5y|r;2vNlP,E6V [1 l:x`cn7_g,1ׯX Cm(-.~UKNFMp <΋>ƽN7΢QYb=X֍;V(ۼ;wPo Zh?z< s;fFUHbҒKt\E:0z!R$ 6lߐװC:\L IDAT^:_`ZIJK776fXtnG3S228w)hZ;8PJe0þgͬju.9l`ϪK8K&?Q_- -2b$ii옽;!3C&#30@""3GˑX'U8VTtw؉K;FS]+S(T*LlP*@"<7O$~jr O@b|.ge v5r}?Ծ&#, \~^ ҃Sg#8< ``!6ukSJm`ac02"9!#O*24ӎ+*{3m(׭ydRKSTĠjQ5(9ܸkwڍu%WES@& zӮy]&L  Gݙ-wC|~>N.w@ ̨ܡŽ\ȟ+.Pɞ,e$#ʼnmgc3reZY+OpʜF M:rAexW!v6zϿ閝aKDDSǑf {l۾yBdT,#Ρ)wវF L\񓗩EuxƖm4xeеWZvEa29Bv9mݼ"bO&:y9Fr܂49>ԪV],}R<Ľ{Ϩ=wم#%ցHM,܉#ѣQ7d9^ƣN#[ {K;-G ?Zn5els{MY;mS/Xzܜ\wl*np IiɌc",> C?ܝK{uC@wsXOH߈ iX0̠PHsqNyD) %gb*^< OF~R2R5` 7E_0 "ҳYwx+<9y,&D7Y`ș}x/uBSӷMiU1-5D? )6뫨Y :KE[$>%]3p8C*7Ƒ.< }GuPf}=tfRrT9^h""s wLZh]9W]'54떯ii4 `9 'k7%2WgC:__,G󇐈Ÿ9|@ѣIg*uǿbdh"#q `efI92\zp݅X$flL\5MͻӽQ'V@F":!u1+K3R25q}4E4M̢ͨ(9d)I%x.[LГ˙5dk`G`݆=84D^|iм] 41oh%+ H P<  (_=(^ [ǃ|^P"=HKtHM\r/++׽W:LMLA$*GŲMҢi]ݸ㧡(J""yF?HOD Rlpý#>cdѤD*FҀNuD&o64G16"Oh4Zn bC\60jh̞NzѶU#O/WPnVmpn\:!ʁ}$|§BC~qc1-9 qxdWDd4BޭiuEqO)dr G#"GDO.gT񮈩ɻ2pG:f]<,~{Ko/УXHV} {ןGѬI/$%2pte԰>ymtPM٪!-֥y:ܻ[`˺3I9;ش(I/OFS$[tR]6r:>1 :ҫRy%Ē/-~&8;A?)+T*|j?\2ZnFOW9wsUj2IY2XvKu?}3ԌT kP[ |ZJz; ׊L=F+ik=)/<puM-q`*s]l ůAۺ-WrO6<%q%odVB'&G 7'kGU5uH &)2nݫ["7:T3.ddg'+v1II>A/Ѳ^#_2tX6, ]xNmߪw_f?;X2j.1\?Rŵ2,OSy|Q.Q0uf?_auuKQRU(!X _malm"ugI:7hKtz$ UA< 426ND&F`h*G 7HCQaۉ]ddeR,T$*>%V3P-m)o_EWm^LvR < Ȍ 4(bEQխ2Ĝ3g4[́ ]SK'>%K kC\pM]|Fsh5|WHH-P3?&wɫgY|T0u9v¿bc}C*xRٹ2nV˱HHEw٢2099ʲ M6]uq/ǖc;ÀT9`و7%lR3҈IyDq(sheׅ\*xn9jux3{P oUlZǁ3瘰x)W;t5GPdŭHO $Kr}YWIԮìk9睰d2p,au& faB$lX`x.,[+RD#<"hgln gG;+O%pw*5.#_ cfQіS#2RpU}*d$| n>=wik_r&;zboF&AZ1;Sɽ+/g"tA")|aGB~@*#=>n3!tD2Ze ]:4gߑyD"A $!AjE"toIgz#^4jYlصS 0}s-KvT*^[b7Yo eUlCc[h]ytu~~W{Ccbto>,۷ҶN ZFFv&Oaw紳aHPZɛg:1݆bibAy$ƒajeU"`x 3>9qÁҰxW1ᔳ-S+Uc0G,Ѫ~c tW~ngdrsr!&JbQPޮ,ݦ59sD'%!Kͤ3hYi2뷈bֶtT5(E)J R"_PFV/ 읋ۜ EVyA1\IviЎMǷsU|<<~JeH}~V˶8s-bϹ{Kprz4V;TqLݬ;O8ۗR9wWV86kՇ#qutq=FÃV fK zke^D kz,emQ>XJ;V; &) F299%~ ٶc#lГZSC&GҳǐL-h11|v[8EF6 ؈#K!0m_DoC/MOޭbGÿcϨ܃܇hG5NZ$XrS 8Z-$%g;dYE&,: 551B__{RݿrUɎ2N53qg_vGKʸZ^ő)z]<ٙJL{Fr;+okVBe,ɯ9{ (י[hvl',#?˄#2C"1ԲpSͅʃϡS?cj`ekQϓPP+eлW2Db=ݱtcܸ:c͛7s5~3V=ޤV |ĩO9zgjغ]tÇ*=Zf]CzZ&k9 夥g`ldX,Xzy#6=?Mİ!=8uO`^d|Vz ` l}<#]n$r= ٟĂ673=k*zdg+>J$vt2;}['?/cDAtm؞y;:1=Fpn I|MPζ 'oكf m[Mؤ86NGf< *"AĈ΃Y}h#\E&~Tۧa}uk1@^'pQmUGk\K`cn͓`|y,~oc}#N9nxBzVzr@K`Ū67bii^nRX$Dƚ7i1%V /4O5wFtqQY:&~us/B_Lj΃ uoc5$&1I>ߎ(%KQ p}YP$O*z gΥD9 =+t sӼϮݢ{㎅~՛eݸ|-5_J\o5?`؅ȥrz5´xJz6- I(s$&qQ,~rp|Rpd+똉NbYDhVfSl|\+zoK(VLa호j:|Mm:IH.=&oT^lY5xi)>CfM42r-֪XB޳t:Bm!̤j=?lޏRSX$~=_=ۢh01y`ӹbV@T<r=4W-% >C\Yhd"1 @yy(8, F4nP jh ʵ{'/| E"ھ92:=}Ehk}39!ySժQd$;+wiA.ƽBy:kLY7wbY~WeX$ NbV_ =O-ZՊ2*wmEjT g&y#|F9-Z&b2c4U(8mؼz-{^q.E0GO!в>keĜVL{*{r+wCc5kf'Ya!bI8űs\<Ob١ωK<{w.HD"بaX,[ۗi3ӭs FK^m3q.Snc(~L- ccOކ~>zj%w/Vk-t*.ٻ^TuwcX2q,Z@&}VKpX8f&HdᨴSW --MZ6ũ3WJ--E~Frp[Ay\g#__GnJǐJ%Dd["iZ=q4D"rNܜ_b%\+cdd@\d* \vơM&=%1J^|MrOOs`UX9b㌅̖'7<_WÜy '?o/‚>: -sƝ$ż^tkZoyBp v˒Qsmʐ IDATx$R3ź2*9v4᱑d)V>R-S~X4j'[l҅li:s1A #=+TsAٖ)a'Dt hCǀ6d*P((sg,5{W̘eSHȿ tm{r];fzaefɲk9@":!Je+`o;c츷ޛyDJ-9 C5&# x@C8Y;dP/ ְC_up$gM|qʡ}Y^:݆1@&֟Vs=f<̒LPZYZSrPT(TJ 9"[gNiL4F❻zMZzF*G{kI!=Wɇ5/&M]DrJfɱ@L4ňMP%"_YW̄/N"V õwz67oj{kjln*cII9r̒>+ dͲ  CGbɟ帺QɅ_ +1k,qf"μinK^>,(O2e  Ewdh7byyˢ[X7 }ج7^$uexz_CƲ7n&D:Dȳl02 V˜y\N~ei(bș̚>G̟YӇf!?g3Zϵ lӗ,2YX`2ḽxj5=+IHo\zpU -e9bAZ-uTq-g)1+Ƥ}9*8\&O^k)kgr^ SӨJxrHF.Ȑk}f fEZgӻYWGI2 :!',QTs z,VFFw91i :߸L(W7VMEd#:lډ7:1M?(wX9z R(EI~`Uɂ~jgϙOwfVEynէG5^Dd RXgU\HݧXk5r7Gg~<ؤ8\] )ٶ5+=y,1bgaS.j5+Z jѼ0%&Կ\;ƀT"پ,_=c$%s'-vȔ5sm"x, r[HT"n-o떴kP+zvgЬ9zaYg[̬B"LJu?oޥUAcuVBHypt[¯ L2$Z-< '6>aܼ!01ہ\{}F}7. Z+0X=K07UFV/#g2x/^:틑-%22sV&%3qw ng#TiOޘZ|>'c1bԨQiTH&wLн9xo6u,m`˂|79b$hРE=s[CmGZZqe*~7SNZ[Ȇ39'x.g_cl1q $$$3kϮ=(;4ã ~O;мI낇 #JV@{ L^يIw3ohX|V}Pb2c xV8HWwt=իSS#RRK1*Ϣ1uUw ETkQC&9d;;#UwYprM؍ l?w ETFtB -k6÷wy֨9u_hJ92EmӺ4yc660Ғwop"}m~09_9=[͕י{4_hBO-%{|+Tauto +bc@jz62өOrXsQu,9q hZhy))l8K; 06glcuru ,?qdk-vz7mb 5ዧyqŻb'[ k*x$ YcӿUxeЙJ^C}R+(%KQA'Vg{害2{5b?{ ({O}E~Zo*< ع3-L_YdRiM 4Z qQ9sk"_cjhP򬎽-aBQ< jC=BFEno9v* pת7l/r4%#K&Y[z|_6Ӻ윾> _(د>qr )-:~p8!XZhm/yEƝXhW|zb7@ :1-Z6ݎV@@mka-TWu+!h5\2^6ǝy#lKҨՄmڃ2Mkf}6ҷ8*~|*O 124 [Y xzTdO~nb~ԭϯ0`L4 NG͙7 ʜ1(LuV J~K9!fV|G^ 7K[v]*ҥpc3P*UD?b3WtGHwWܦnrZZA% sD%Ġ%sC-HiV1itn<2Enz[ٝw?\Qk(s el?g`eǪ9|ּ-\ 7L>sw.bZV; I֓%Yu (1[2uM~ʩ($q-=`Rr !;G;13DVTHJKځowyb.vXd;*9ճsG HӼkpy,qbFPr9x;{լػ-I!d]*_ *)gȿk]@,">ex ʕMPF ]Mm;-M t}7]+ж,ݳ~6@Wu<ދ\~p]F~k=w") DR[i ``mͧwPl;}=/ /DUHNE]VmǭwuF@`rd,EhS%#vqO^>!#;|RQ撥S5AG&y SzGO9{E< ׆z.v˪Kǫ;&`ǩ< ft!x9{h If (GDl6\%pEQuyAC+$72)j;t u%5|i.4D|yBB󊆔a!V|7@o)](S~MprBGOg͌~5"^АeN4qq4=xN4|/wS~s ?n%`L$)H$zyc:9c.8ԯC:UpAK7He 9m4vά#چs8n.y_?uhyx4t}9:,̌\޽~ |x2ur} }61sY/,P.u*=7g CMyR̚e#3zd_*Syd(+e%^ypG{]F,C> U<02!:¶Ru& [\Ss۬=y>") 1 TJj[oNP[c徵l?MԲĕT\HJO&2>F_M쭿 }%U|06/toGZb{܍%"?]|cW1l=ZM-y1N6Xs5j67W]ҖンDӀ:G-b8WT")ՈԮ=+ڱ=W+vz^n AtVF L[!IHMF,8ԫDtt1 X[_jU~>ۓū0ؿ (8SHNx )zWDDr9wonz\KF@G02&fd&uXZxiJ p{ PPzO U,9w*$1mn_K*j3u]^>#E\ <)&%qqo4 ,bڹda}c"/%|on?dE_< Vl Ĉ/4Nco zQ=0_':keT^kIڛQ)Fgos{% >]qvϒtX(fn sSqs-Qm۹#V̘=.rpU*xXPSKC u՗\["[Iȃ8.Ԕ V0^#ώ"$%[ &)Zf2tu 4i IDATQtOj=qx|sU;bej]L MCKR$*!m'wum~MLb,O IFĜ4!24ZT>fDsqw%+0\aƱxByMFy\,lpT*~}|3 )gZ|(߉ψGQcki`eb͚C?̤+І7^.936'9=Zd߻"L^lhtrJc?pĊ j2? W*JfYԮ\3?(2%tY7۲ |2`MykVSwq7ݹH#zADdZwD"AE,mXw-^ͺX) 8Z 04 RWᴩ5V>;G"WR }1uen;ҧYvQShА>-?$%m~]l{339s/NYS,9hjhB;IJKoMꓩD0{ \Tu>bɜqDJ5w3[BҲ IKHv`^ʞD"AѠVt;[9ӠI ?k.W1~%h4 )QQq$lфl ou/gEd'\14gt5@'{t|=r9|CVb2;#}T|  |4jW}w`[  h0X0b/sN8[n]^9A,;biœgNFrtcjꇴ>nYO^pw1mn'$?>/)R3AG[S0g:~^;TʰA]8}*;^doj2;gscz6DgQw-28@Q0%!1_i`=VfM\z2樈 M GB&)&3^bnfڕ3p+"imÆ-{P$%aadS mBfҺY,Ε17,Y1ͧw~(BIسRKد?EtR)go]_]\8ԍsi_{P/ؚ[fbm">ͻRXsp,dVH iY?q;wmj,y%s-z4WUņSѮ< ||ݱ?}Av"߰[O[#_0c|ߧW j'3)=!uDŽ\[6C쁈囔<"6?eK*p˶b˜u (`$^7\(PWâI''3tr275Z E=S#ZOB=hcTA4nX}Wfd+r 5xG2zKPC%T"HѩNy$v=΁çQҪCzMÛv*E,xB(Tb`&VzhLALYC`flſZ,7ưsϱϪD,gk5~ME0$37dݏ0!&mUA"P*ȫ+E]X|=˾ۚlfUq|Z n9Ϛ11.;xdr)EWp.#ݧ>&)Xùclؼ#rzb0J=LwNmҒ3yI~=Is~ŰٟWYViyEJ) ,5~??&e299h"ABj3=)Ctcs5 016fanڂV!JfEu>*&_N좚ݛv,'ēFFm k׿ A"A SCc{={ZjNÞY9r8}U`3S@9L5\ ZʃG8የP4{l8 #; (rsݬ+WɇֶoJHHaɰ9hɵyz/u-,l_ kg0_wQ˧TUCE2!Q/wO>W|?`:S6feE+U|I*S*nhj2ѫqwr-$Ƿ00Z2C.˟cw?nenCTb̿"=}E,LJ98q-&щ}.7GW*WmK78keL?^~S,>ΏFR2eߎ0:DJ%bnc9p(?QeBؗ9LRHK`v4+^ux]4j &FAċW4^RQ`[X9Oh+SpsKcyϓ~$69w/ř[=&`e7;@He8Uֱ^z}%C;:]blݑ-4oheR):ItR&FؘYcg🼳92Yibb`Fp)X˘0l̬4qU:kr1VvȤ2tƤuhUmX2tz( |m)cť\7n2h&GU۪ c5n? ;rx:š}ئ ZZ2oFyCJ8VDqR"=̔4C#IvlJrm:Zhkl!:zޚ kZRQ@X~>1y@&C#ؽ:OFzE+)q |>nҦCT*58ºçFFlGO\j"?Ng!ԮKaL * E:Fow{ N)W' ª;iղ>fƟ\5Uo6XS a';|vA[5O̱0)؇^W31>:oa펝?wfCL곉nѸRzE%u_$g}B)O?gռL^١H>rJ>E;WHDh4kPri1И󶡫WHCLR,W`pE[fhddgQΙoA;5Htb,:̰J$4K/[&sfJ7F7[ HiQ)޲N9 *9쁱!d9xwb#1c#ZZ%< -y6[sk?R朿ɂA߽wۊvȤ2_?E |\?]w`š%< Pӳ*.z:@\+[S2e('6M;.߷ΣOPIc fV7ŻVaenNj5t:KK%ӢNGBvq'+;=]29uEp Ch_aؚ[SѮB^Q#ɱ,GϦָ- s`a2pHyo,69xY:"";֦V\~x+o0uY$iD"Ϳ'aϘn<S{{NT٨j.?ξs5.yq]Fиj}VO7q5&uWXV#,9"s}Λ1kƉKQ7%r 7_ sR }3c͌P0uQh&7KAnEFćFNn&~s*JMNfD2\Y9_ ׬im:ErrjZ['{Ϧ89ßuOHŷ Xxֶpt/ Ӿ02`.8X~wegx,\ƄɽujBAݼ>ޮM\*2|<+K/-ۻb{2~g=~y;}0_^fiu^Rŭ0hlϴenvHf ߶׃ı7hޡu7FWO F$:,sqthU xs;1<  hzSug>}zĆ_a`wYȻ}'.0`wԯ@m8m;09~qދRrz{L,-L8mYcme3n=6o& Դ sPHi]LÂ=lSF [i"y.c(Tj5&%.T)J&">5fdAh7gϹ&'frQ0v4ނJ,Eq8XЯ.~UUl8ng$%cmj}Naehi#N. _>ljÐ< }̴㱷,P=ÿoJĂ3x/X1wJF¸h:kC?3CAUu7n ֶ)e畘}GE#(gmwSh49(B!L[%|w13n@,Ḻ4|z'}HHc*ڕ0K!>%ՉK]g01';ellmd`d e`ѺЯl78J[]j{ѕǶ3OچQ/INv@$KM7+q1j{(ħ$E5*|s3-HHIHרƷQ)g^t VX\Jڕk"JIڴÑTWG1rDG>w֑bߺbWTj5J]"3-m~`H#jU"rm ?%!;;G <*9L᫇=-z|453 =<753{r5o&CzPtiԾ~9 &\amƔcKgC.Sϧ4ŻV1fd~r)/_Я>{OdP %6YгIW)YI4ݒWH%R6.{p%KH1G#UjYsrU(2ʤ\0WݥVMbmPơ B^ʯu`Wѧ!A GB\tϟDNYpgv s9#65ee~#z]=LB@;"ɿPxzLſ:Zٛ)3WҧGZ~sI%?ܭ^<2m*w83/iS¢hЄt*:4 i)X[ТnCq# ш?ܰ̚@.3gbf)\HAR2a 4lG5pD9%@tڈ9|&iFTsCyB)sEĂi_M~O˕'Ox?A&Hӱ@"ڣM#~;HlOFF6 ɤ?z?5ۖ8*`j{>>!,SZҪs d~"5O">VBL睤r@yk^ 0A&"AE*An밅)cS͛f}lںS|sb$C煓I2|JbC[+jEqSR?* z)YcurM!MJĒX&Oc~(la֠$8.{N||*%!8fF{M;_^ii0Qǿ- , e`QQE- Z&&FZA&n4֘U3Ov<{'Zr- &2En!Q17c6q?/L 2@#<}WU;~OҲҋFuC{hK֨YshsO+X? {Fw5z6˜`o9qq$ 6g(H$&4,r6@V\zx u*Fyz%ǻ;O tr< A*E@1~2_1%ؚ[cƦðʻv\kͪ5O-\p]2YD%DΠE(UsI,M,d)Ii{j^+j4o/Xvh1YJGgkiIBbv>O olA"i bȀRE׎-8q \2JSQ6+>Phaήy @joG߅@P) h\."l^x]}-삖?H ]E7t1(%Kv u)nɕ;7iX?sTЁ]4}Ct>Ukέ߁A@فՋW4iJe`|i$}Š*/y8ժUaΜܺf٪" /[j Vvƌ_ږكvg%f)(;bYE-'1)_bo5*CT* yH8QqfFadg)PhD kU'E_D$ A#jHK )%,62 6r)Y;-k٫t8kKVs/s}tuh\_NJ蕓T",.)L֡veJIFvݯ$V_ + l o7vu[.\Ǯ [Ӓkb猋3=̅W=AߕHzuoґO4,-wW,96ѹA[N8Sz;uZf:ٙ4Ŭu/͒sňLQA~Ј:.twte}gLSI!Kݷ0JHY r,4J+r-?JJŏ{ ^ZHɿ& Opш(^?OpbɾbS'mE &ee(#P A[L>'r% i-&vOZlP@[M:v5k{~䪕eѺv"S&_;+2$WiиTFpU$PdCyk9:_F|?_BŸӱ7h08o[ƲKR0/ru@_G+;bbIHMߛ''s}\RqtwVF 2ұ3pfBjKw _ܛw(O#Hߝ(qCi%&WOh혗jtq)G<ݱZ\Tr &O΂?p1|՘Ǯİxe%;`x8eX޶- p%$hDnZ{N0yrƌ]lz ZB* g"8 ,_j5<9 Rva!Tļtz/ yʜ}  #+"Agћ=To^qB4mTVlyq| Й Ul}~JؐruTFrnW*%؜(=}- ^<~Lfv P`af)YyLAi |TjN?ӻyWZs-gaˁ "4=˹.?ȓH%v܄Kc] ؿ>G.Τnc6@[XN_ 0Md2)?bITs1yx8\rs5!/$IiwO_ȐMpsRbyps\32(0Afbljg ĀIl24ʊnlnN ( @&jRi_ =Cb[@_/P޷ ƺcBys]}/.`XUCahcg*ĵ3e9V14Z5`(7CDJX.0K.ܹ̼Ջpu~ZK~7 ;p<cpY9UD>z9yyL_12V~  w҆z0!.UGvz@|;{zW J ()+r s)S)120/GlEpԯyص|' C5|*BDv@f]j<ܚ&lxM6131E&<\:5jr#[hdj)jsMǿ%;V󜓙KOnoZ?5Tb3:NŨ+F߅*{zӻ]g}NG=^9ԅOa`5jB"/KH^j9QwmxW4dzƄoJʫ/ª`ijy$3r|w'_Dyy(U*d2r w.saj4)ff=(O q |C9o2My00OOGs;+[܀nM:cma09߇{)d䒖{ytikS05V`0FadTjYu;{Na1Lʹlܶ!uh:\Aq^!^ `TЧSSOԱ ehG `h(,PGU< XM̍jLұ: ר/ 9q.C`nGѣkǹ $Fر ߸T4 úSk:D&EѠ8YXPOd2 *~5R\XLYRRQCRTT d2D]uI R 29HIHNag_T"f>cGsYMX R㗔WFh'ؽb> Z؞p2TUFNq.e2r9FFrDCi~W6}_'6>{ѭT"ER,Shqj/go_`ςU7D3ϊ:㵨%kQ&FȥGolq"qTaŚoSxA>xs~_HRF AX-ߙ?:"h 3EQ.|7_JͬRgţOdRR)C ԬbσLd`gmM^a>~$&prEaDU Eb2pu/7 "b٤Jg 9_i 2:^AO-6s?-{ؽz7" E"΀|esV.iH'ޏ?_ڇҨ8yCW" S fݼ9xpy ) ,BjOa҂ObiUWo:xu;W'VMg \B|i\u:$zu@u.iSMN' 3Q"V.X:blew'[FL7GݘjOScK\O %z1Bo(STLځT\8s9B%dfGdP|^tGӉMk+}hA'M͍(*,}aC2sv,g Еw3lPwdh9&񓗸|5:Яw>M~bGas{u/G4ðU_ZܮS^G-'̭i-E}gsz2ERdxsDWVEet~K"يKyLzk(~Đ]V-tGR)Ʋ^OM@4Dq}dшb H)!0XV*Rq\<ts= PvzVi˙DF2{l2+U4eJ\@ 3VNvl"SMqF&꜇2cLCf]*C05# @n܈P3)5hD"掟{(Kk挟{~ƒwyHs<$"Xwhr +3 bp͞|Ve*%mgX^Xy!$\z@INǛWL"vl4~O}Ogo'3/OFN'-;33k뙈"cC ߚo{2X6JA`tz}-s#ß6!5˧u߲6 ).+F}]Z̶S9r8ZK3 %`nbƵ؛Zwi[%ζ| H;v.K3K"AJJܾ/B+2Dم wEe[Vì_37LΤo2Y{bbdLrF*RT\52r3Q=3!!rғqwp[mde17<nc͜ď{GP)ZԢE-}D|V-ET*no݋{ױܡJzf|3 .F]aHE-4=w3/l˓PUOK F-zrZWtT+).+RWce`'vO]:5ǯS?Dg:$|5D(瓙ޮ.:yˑwxaR0HN*#b`$+2%:|]o\fǖ#TJd2WAj GӢQY6L-t?w|$9T&CjmnaP[T DTzO$:ލBɈMɋ ܣһpsB&R\RQc9Yt]D~hPk@*iyd޳ 7 {qP$Q701Qзwg̘NP_T*(~^N&_||S s.Zԏ9H 6I_ =܊!X-fj ASa|6lPk7ؽDyÅ1oat.ٿc:Ɍ 1kY `_@LfPNjDTW$ f]ñ1ѓ%@ u3"c(^1Ӊ̞Fuڹ0QŜܻTxI Ú#a  3ѼI}lP5 8QSG7+k3ڗO-«ꌱK%YZn܈f6\/( s}Ԕ }3O$_*cg2Ү&U2#c9 L@*yDUGXi* P8~Mu1չ9 #>~_٪ ޺r֛PX\د'ѿu?ƽ2 sDtZ_ԋ<G{N>*əiCk`fbw]F P`ӥe?udϹ\6z@G7~|+E^bqudw+UMNgWM(SR*@noNܾ_w$e]4mB^Q~`Qi1[vgBs5/gџ?fLFiڤȜ Y+ Ll>8T=uOs]==kr|"Z9de>]w$#7#NZ$ EAN:&FƜS{u>{*6kВHf t3Mg4xq9(dV[ê]{062b!8="d"e*ḽ1 n&۷Lμ}ABZv:l?0&\F`P p;"9~""fv֘Z@QX腙rC9wO^&W;hK7C }u@ 5XXC@&/su:n&E?W@ ޾Kh_s>W7 @N'TPU&,-_xBICИ 0zK'<(fi`}6/?з[ah$W]?s־/]EҴqno/WnގE:vǽwIPߎbdDPĆH0$Y*oTYƒp!S|w#ڣ:ڶkM= *>K7ɎcGY&/WXY(l9 7{W @ӐlMǶ1b[tmґ }FcabNaI!#E& ZfߘrvSբsIhօǶS\VBzN F*r/5„uҲnSDQ$&9?~ۻ"D (C!3QILOfޚ9p(ndfr.2nDr7)8$"tXtD%D#JQUYr#6.E11Q`0Fr9sE8:0>>XJ: ܄ϳzمx9z0tjDYJE~DV#|{\EƁ&s X m|%~W[Z!HMohG~Vq2qH  &ѷuIeA'긛KF^:%,x:xhc_A@D`J{1*SH9{"Frj5[.߽D"DiZB<9y jORemgcoiD&R\$ wa?| jחe~E:N j猫3xTYƊݫr|I ^7` 3TUZ=uQP6=Za lۛUdtWpt+Y +sÖe85]%r9 CrfV*˴-6*LN)))AvJ;5AgF<_E"D3f&dc` tpOfLj b?t._*1||`zOUJ|:#f43G^-$_)Z5@!{N/ow'U|+Vn̤J؉6\L!|4;Ap?|3qpF|XX!gȱi#u⑘rǿ*.,#vftxsln*tԂM[6<;rˬzs++l޲' 32dk"@j2T7RK.'k@uސj }o|1+ PL?o,ܝ q "*)('\y9'%z݇q^V)prbpəjɤRG&i 7q\EFN*r }Fs71]=>NӐFx9yBV`ؔ{[H ˤ~c)(.$:9ԬTD~?Wt]g:ç00e;Wb 3}D+.G_ݫeVl;71᏿/=wsc3igъQ*ݲ;!zVBf)vVX[""9106TTzjɍTg;c/٫_{q1V(0Pp3.E[g?QttgD!Uc)[5#+ЎZԢE- w6(^2NHIqw6<;ut7t>=+qsr@&G"`cbNFW :-9Vʾ|%!b0˩Zsq`s9N8[|'Ux*-'vD16RzA_\@NA~ޜGk{4: Y9ڸ 8| -EݦlsGFoZv:G.` 312FDLO.ٶ[ k.F]e֯_ow`0}H 8XS,C * C#$_LrY˛ȥS|z:#cFt3)Yʤ)9 5 PRr ^3 rhF'9܊cޤzx#9UQ77؈icl`\701>[OJuT*EED5,n4 QW}օ%E$gQP\ƃ֓ Gqyz6qhԜ{Ns~N7&{k,l*oh48Y1ԭޗpML-, P&gl]NCt j=4l<|Y8O4ss%"-ԯ ?~1.OןyŪ;i"5v3a :t qyG"tZ"Ulf;x{&O>!,;k+ ^ړoXio!RD 704ӳ[* ;weHRe,]c"({FyQ ֊@9VXX҇#nT*sZ ƦxX4c[̩Ѷ¦]s> ntZOro"(ȏ^_yWmafsЯσ$cjz~y NfN^ItB< #CT*5& -GOfQFϟȗc?W ʂKF]ҡQ#Q! vXu- !~^* / k9W3yj:s3f{s-&g|_mA1m>'v17Y6}1)i\IvA.n oUi^&ҷUO~ڶ̼,D_gܺIrF*%e%XZ`gi[)KmP+FwRק9| udrFA*!A '98T"ǔVD%DW'\ |j(>3SiYIZ|qi@Erf* }͍ywx&}7ԌtJ%ؚs^$r 9~$)t (,r%٪/Q|DZNUEP,H~ ^N lOGwLEӷs5<[Js&Bf^7nq^$[a 5"HJf6뛇dk3+7T2@/_~c%]6D'$2SKTG*2Cꍟ[pEFt}Ղ=OI3h:8z51-*UE()Uź9},Q щ!0q&\9N@jtΣ E5 ;C?!Q]q'af +;3 HE7:?sth/gvʾs,BJ+g""_SILJd\i" 7ꟻK߯$ˆ_߳YH36:r<}4v0&%^Uf߹OG kQZ`b.G^8A#©"\]Զ6Ȣ sk eujLrdgEػA]{"IϭFRUH˸wČd+5$~ $gp>6V4 iX=YYΚҷ+( EGAL]TntR ދcgmCX@0s1`kaL6Wbs)*tiRJi/=w)&ЭI*SDQ#|DߑJpNBԕ;}yټ?| ֏QeV:U"<8}n|=S˟psh2 f hZCzN6H p _Ӑ{*)8lxc邡\8u2c_J}ܸsA9{Ba`X4)lh- %% F)`o+kG&,$˦N6p IDAT,|s'tx[2sh^C)bJm%u3@d[ǟuEף0A٨>#n=qZ ^AެZ^Xh!L%~x?}0-!4#"2J(ZEG8MB_l_bR7ewW p#TI:sfMꓭ?|BrǟJ./SqybY{cljكQt֠u!nR\P߻ A/&}]Ftv@fLAn>wǔizof!a fmS>}(ZTzREߠUcEN~NBbnn;-"qS۾#&&΂u<[' Taal Lƣ)bge3ti !07BOpzhÏL8GG?E% %O%\:;uƫ6^;;2J|mdmd$#:R5s3ܨRFA jחQ|32IHUzm&d/mՓ/<`\%gf~unm8 +( y( aڠ|i)s|dWnaF JeT0 L3622M+M|wddgL&'#;пuv^8#G55oY K\Ng@mG$\29m?}vR^|,Zz~"[t؄ ,|-U*6 $1;d`>qth7P|4Kw/֥DԧeXSUNe})L&ͷŽL*C,/ͫ]Sh02xdd:[<+JؠG.`@> _zza#j sX3Vz7#c3:ȥ+yPsufG BV|l=Oy d4l˩tAbg-/+=c B.V(1)ĤF٥jb`dfDRF2!oer% #Լ tI:5hF@+Qbmnǹ~]n/ iߟM:t1G3] Ʀdgөa;COF۞S3r3mZ5j&Q 1=KQW4x(q㋟ke"Ĝ ~ޙT"% ,qpvKJm*iYw<+h9Xgi왺#c#cXUߩ FԪsqf*%KG*p.Zm)nGUڐ "/ҼNcYup~4 @o/se)TDn_L"KЦeCVxRtӳА;ѦUC #2sPg5/?mW0R?HT$1`"ΊwguƽGwM CO@O .v)[Z*D&s3| tjw-Ȥ2̭~Hk,Ym,9u˼Kx` 3Cfz7Bpu૭˘79?IHE쭋L7ԤjW [w@UȶcCXZYq @|}FQ$07z ɍ#O(%E?/& 9sT}SZkV- W;gl-m~jcNĤܧKvXYXpn4D6;WRjoo,.Izȉ'hVVRf)P ͇wpezMo j;qW[0* IEӟpi6hc} Kxp9 Dm,/Q*/?ٜD7@._/WÛ%bY쎥֥<+ءOmenCߩq^ͮ:ӥIIZ9/E㴎hD 22shެBgNF\iQjy|m9ǯfƐyq[AZsa1NˉkgHI/`)P2Y8:eZ2|/k k53O.xl0mzS i|Gt vJZ7-%^-T[1Zu#_CFq+1WW` mYkFRħ%}:+s˴.yPʕ׼pwfHq h08 ~w'7ɒ  3+ qIɴ{$r2,#F1L8{K[fX-u39 rQ/y @ԣMH (R*(>R j8xZ͵72a^Mae׮eԸ@s4 !32RM+f M1AtĦ`gecb{̝V-ш[P\8|#c녙нkJߥSKoK7h8Z4k+)v۸3E`&R=h&pˈה/"ɿgK7t[VR>{d&/[ u7ukwqr`^$z^:D$ht-տz"syhNyE-M*.xVKSϓk\VJ'Ӹ+/'Ұ6z?u hټa "7 Q9t,GO^ ;;4h@pkgѤՂϯoEbZ rwv~\碯swc 9 o\&r[T<>M4j4: ^uzt'O>JRŠ}3k4bҡQ~>+6Ks@,|?)sf%~j _S*w^!&!x 5 }ެ0F2dD ؗ񋧱~fߺDDPj9)֣;ħ%鸗ps+~[3K]MbzI|ziz.+iV7*j[KkOK$6%\ڎRp:yFOL| Fə)x멏:J-'g^C< 6-Α݇%2gvOj5vHek3Kںʌ!oxzFZ)W2}d5-_0d nWT:r׺/֣xtIҊM(Sa=r Ыe7 L0Nahd)}:VZ TzrUz햃Ħ 1`Hd1:a~ݍ/suDQD!S*ޅ~^H]ķWj잷d7s+T{FV~6cNV/3A̍ Ըg`QWҨJ*5Mdf lTiSi*&|6IYqa/ WS֍Mz4Ӏ~GՐƙ[gPi9; ޅz+kn}DJMh0}#s Z2}5@"HHN'='Qvj9ˡmԲSj`4p>!1I8z?YR4ĉ"mbxL l6$95 3s%]©~a%-u;6#3.ơ<:Cʙ+VM E 1bP|lf>``9 l{E8R%mt')0.V؊.i ! R _-"Rr:f'3a̠j;Xn.:Ww&m$AOX`2cauQ=Rv@8!^W}OqX!W ,$ Rd߳ BN/lii|2BQߏ0󩕮J}Aq}o/J*« u73QW8s*#HBzgl$Zchɉ?nv*m{adɖYS)QXZP %x3x,/'nrs8"# a`~]_A3a4c~O2sPihu2Evz/~n>wԐ~klH`F9oO#93T 0r4Ahޔ]sfWӹx*s/br߱}8](oԚV4qt\3µ\}hB 0Vk={QG5lZa۳7V&A pr){.|qq@T~7W@k k3SeY~=CJޭ2첺ݩV}Ɗ0޵J]1GҶAK ͦeh [+Q4r]Ҩ ޼#5wcF{+gGIԠ5` j_]#.#D{k;ditglj_BӐ;؟!616bTQ4KM0Vp#&EVX[3,If^6޵PǙ0ys+waz8p-:4j3QZp͝'ܰp>cgSxzWNq+6R {'<_*2#'F0c_ `9{L9˔'g7E p # gn%jl,X 7W4^B N6;w;5]dR)3Fшk[]FNۈND"E!W[˗0Pr 3D]`2]EBiQю\>AϖJZ`4afte.DB&HF"@bZ2wJL4qw~_rg87y9 [fڒ]J>o4~7\Co8zqQfQMIJH ssT٨4lP|pg7Lc.ZT]fP,?/(Wg:KŭrO;dkMPXYbHί܄!;AnXLw`U|?ȫQ&1ߡ9j3(EU.#U/gOv +W'a-mtz>[fFf8/ nUT\޸umw]&b2HNyZN Ȯk48C&$b6WrErI/&%8sUWH ;&M4drT _{AʸN'.>Woq1:WуDDj9M{ i5CzHeғriڪ8fR=OUInr0օ/3S'-,q (&Rq\m'tM(,܉!+dFEFn<ɅQ4*to W_WFѦ^s6 &؏pr+5tҽk h7GWa[Ts HHA&a4+u6 H ?mF7̑'2{ Bu2OnC~G Fr%nޕj6>EbF2ÚW;ظ-֦Fb2 s27/m,?-š%I'T"ePǾ;LDU.c>Kze5̓<V֒܍?_FW=viT\)!3e6Fԝ+-5 mdp؏+$^ʔof4x!ۿMBR7؟?HŃG$g !e_.(~]ʭ;!/Co0%R .~AU۶}>hFaكd`maE-pr9X`n\&#/#"NNv~t󼘯 IDATRY$f$ɚy kZ" kgXg:z~uqseI0cG zmov;Wq7d,8TىkQH``7ӿ+e/k{2stdf[> Sa"C1ؒ`eapfq)Q #:"GIer:Lh\iBhH]-f _{C%hQM(-FӋiEKr -?a۱ӧ006i^3i>>z5LV)hr{wtsu0xs|~"r *pH'ÌclRQ @=z Zp_Я$h/{{h5:sغ}?jFCzn:)i$e 6B.ѲG 3+*8wr dɂpdɸ[ W ٙ@om&&>OOh4Bbcj 㢰47QH <#AUݧ@@jv3Z;hUpoW-"KT"%!= 7NJ'spup4N+Nv Jhtr+th;+[f F=4{AiAϖ]rxG\l}:-(%3¸~ꗥ" P21"]+T ^:h4>&&ٴ0(.76 `߹KRg[2c Yjos, }\s> ֛u{6`o;)ֽdXXfN75tH ja.6/zͻRcޟy/dQ/ckkҊG\rI ѨReA* sI}pm<7GW>Z=g;ӟ(ҨVâ'\lYh 1tJ,-U6+DQӲbZVZ3G.`ռc cټ4G#fd Kw2kU׷W[?T|6̈Ch޴ 99=A^d݉3JVaٹ9:KJf:nήHD|[ [A@"D`4Ȗ#;xD8{%.M֪L9v.ŠFҜ}l-6hRI( .ݻJV~6[?R@0(!!x:{WB4j߻HJ~2^[º"C$ \Z+rIb-?g2XE/?>d (*[iks9bϛj>+;[p/&u&y(r4Z-.da|&펅G!2:d_ϧmeM`^UοZ=q! ´v~DB{zF6rR\r۱%ne#^՟w1TlYک !ڠ,zo+ӖwEZ$o@C:bGWO*YȍJT|0.ܺ A^uX&_z>"z~c}kJ L} =ǗH$1)G5_GzA*[йSy4k^aねlrǭty`5!kPBM+c-ķ2BmI x$omSEݺH6uZnmCx`0g4$omc 2DZ2w'>ȥhgdNbTU}Y O.h4"H$܋yD:Oe߹C,8k +Z5J )ǵl3OC |jyѭY'5`4=oKͨmԄRA`ye(ߺȺnĥ& UbnfF<]k",efYnr^4>n^"OlAILL%c. L >x` #Eo}(K᳟2cE#ͶthԆӿơL[Q1xߺ5(ҨsΙ+yh3\@`̨v;?PQ^cw $[2Qhbij|dV>z/un˽.߁O&XxTjhYƎOzُbY&\*ף%F)h"% I߽"uͺpr鷰Hˍ qj}ͨc~wU>ފO n'=kEc}x{s&>Rr,1 D5PWz0FT8:c+P0oWĦ+?E*5j݇ҸxBm\‚a4'#'IڻL]x .^3'[r rY9kl,-דhL*+%}'MY&b0VTXit6aeKH5{6*n}8x.'Շɏpq .-A*0$ TVXS>-̣ex3Dvu EogѥIjmiВӖT/Vht"CNćdf픅x;|jy!ՑߊoݕOF|kEcbO"-r3%/OOo}}dv\S_UBib /Q9|S)0/嫷,ʜCGΡܣR 97' ԍ۶ 7obºt,E2b4"ni4b\4g[x'r2ϐC`>Hn^AqKFUV>k{pnލՠ^ 4ty؈F:7 }!h]U+գrt31qXrBaQ?q9؟xjvdrohJ$_|:gcLGJ]*J1Zp->ͨCaoo"$wK {%_ByRWR\Фڢ}~Ip\2~f`n29'/dbSʞS1߇K(A|1{1@d2F) KӤWp; IO#_~ҁ`&7VAmJYb4ZH􃛄xVZl\4~ڿƁ@];Wh԰ 0;?wHϳsj)(ˡm%=iVć~Rq{E4.:lUN<;b y_qXlJ68Ӱn&=^3b̬aeғ)-קq&)T"Eo| 2AuY]ե bklZy;_?]WSxol (O{Ϸw [gu INk8Ё{jl_Rd\"'0g×=b NMjȿ5=,}g jqxv&Wߤ~:jW)Ƴj9;'{1w5]AɚyIId4[44LNrf**ק֪x`+yYr AX4s}+֚z )TsdޢOeшܬʖ\޿ħ'CEҲiװ5]";PɝC392;q򪃭5:%[q#6gOIek{]=E`ge'#cX0~v,x1RϷLNu=s7,š7+ |ܼpFW(9rZYٲlr[3 |CQG9v4əX(r\NZV&.DgT&|v [Q?κ}Xk5_OGdHcZ5ظK߂ r>AFSk/@ jh(kzgwrQ-Dڧ6Aޕ&tV]ڮ4;'bX֬%'GU17o3verN^ak:uhGȞ}ySprV)gBA0}^>L?mфFxrCAi. rr2 pvrdZ 0Mju {_Odrܢ]&ϕW85/fӰ"F@s|2glZ[Ф%(KA@A۽݉ E2óhɺ+W2cI鋈x0M,Si0"r D(rr&g^" і=F꺅?0tHO@$I³qә v;jy}bՔ#>yZطz .s rjFL)E$c&!/p s'[(9.Ző ]V|o vSf % z!,=l_I]KMr (Ҩu$>DTx IvM˶n^xOvS{o(bgeKHĵ3l:+6 Zs2X=J ݛ:~d0 ^mӳwGӐƜqTsNycgiSC TJW.ܺDFUm]cWNS˝Qon,\'wr.%(/ .|5i.o,B 'iܰZ~p{7urIcߥN#'`΋ЪA j߿RZd͵ ]8xjvq`ta,ܴem #e(]%9#BGS,hu[ofV7t$x\ufenI }urN7!^3u<LJA"AcӖ5 TZT(qr%5?1YA{s=g!$ B }pwOLz~9|}>DСQkf }jB~8AhUDJzi]9)qlؿ' )=6 ZR'n^b%BǯfwlɥWMcLW(Fl0xMRpwVХ㜸za|H%Rnh4p z Lnf 3{7RD{laj,Vb!$&)W/$d( ȹ[Q,ݶ$ӦúTW)xMOoBua{6)#gܤJ2 )M?09Y(ːIl9_ 9#++vkExWJ^l2M^9~- UWCCjr YY<]Ղ[؟O槝|r+ ?ہ\.cleiX[9:RA]5ѻgF3FKAA*ш=8(1qtlߴZ Sl߶MtqpooHrJD]Q)Xkiaa|R7JIL-,`@HG7  uhDT![?*"5م*ҳ("D@7ċ4UNmDD!p*9Oal/t߈f B:E,_f`ƚt26ed'U>djzGM$^u( 4bA J%Y n^SLȗhV z3s @$9=4&Ð"rpıVn}ڽ?΀k7}śƞubh={$.(T!) ՅdX=P*+'(TFe4~B ۙoީB X_7`3ݹn ;TU֡iٌ:>?kί&]VDVCՠA ϠI{޻u{c/XWcx|W\!" y5zWG3oPfF`#} \iF B= IDATtyxsUF4#/\mD$]ϨWK;^))Cg3}$fU~ۖӦ3J++}jyZ`:tmǯf- ;@]/ux`FUO YM$0I1jTTIhpuu \+cGtY#S,jY1qD1TOhܮքMuϠݘ=gs>C7 :FRBĤ46lޫu16[҉"DS.:?F Ct/[psm| h$K72G% 8fSk݄E܏WI~FOai JSvs}kA[zA0zbW u}C8|ޞ;"IېJʃtZZTkn92X?pMxojJeFLܪYU'cTa quj63/\ޠgokcWN\^_7os+ʧu4b`qtȤcK*Ѫc\.Sc8uҬoXl-m%6%D?ţxFelu}r+?eWvKY oTx'Yyj6WJ.lڷA^ Lɲ?2mv`ҫcvJ7o=Iie:l*]11,Z1me6֠g5+v{-?.5 >KM?ye~[Q;w K'/7Pk5()k,\@yacf#ӴiQ'`gmZ.khYaenAr/z {b9l3MgYRXmnd^hU蟩ڳrqcz _OqCph ϡ`|*V-a[~]J$B.JpXͫ6Uw/ޝQZך]Gpz,͸vy#IޑlEߥE,]^{x{/(#zv_';.\,Άh-'^Se2 _A~C4cn dlPqyj@JV*nDѫ+̧u`!I i\ޥ|$^B@!9ϯfenY*V)3Fgidf1e$Q J8\+R_TkӣEݾgg|QŪ'|;uᓸY"-{Uoh߿DODM|m!ށ[1s;,m Q%f$bXbS\?Fe˃|=L]0v֜{hGr‚M_j nT"ztױ X}hCC|6'B1}EoۿsSC$B]8:1U׳eW,qV^ŒuΠ JI AU.bÁl=A&gQrk;k"s.2Q$A@"UWS3hjjIeDTQ*`x8Ì2h۠Z1 w*6WѷmO&}<]=KJ5 x_ˆWU؊`F(l%ڒWVWT(er*uM:u|^(""ۖQGB6A/}{ՖH>fެ=YH8"cIϨ7is B:k{OWrwj][bcVSZUFO~ ҏU[o6 rbH8F;NZ9޾e}۵SxgR+ [T$tv|= Hw?.⍑D)Myg>y]zA(9Kw*CHEu%_nظJ]5vt֙z0ɀ(ڸo|291aQXm6 s '%йuG\Ujr5fހR@"Hڬ>ɪ}_~flH\dG|*MA=ٱl--NQ.^p4 l{(!ڷkuIzɊ^o 0h< \qrRB;5B Ex*Pxqlkh2V+\L]wLbيW׆ 0cjxÕtڞ^=:6>6Gֹ,(}0id<@9:9zbǃ$/IRڵ۵ZTkN[{t|EqAU-\;c6Y6DҊ9?';;hwMࡓ4Z"x86}ՀA߼sE 0 \&b qdXX,v-Q›_tF=6Q$7zgp~ã CvTp FԥfRWbj"9NǏgo|W<9!b?C7MyS,ZLD̥{0; @@g<;&4 \ 3p=3語89{yR70mz0SX\Wz{w3uX_Ćǰjw.C$.7{Ш4q Mr/*'f8V#6V3zBa[FN6G_KXHL*B[  ^* (r((+6"IDF]eoߎ@o Çvj:-cݟyplJ+ޮ `af\w y%{<0i6O/}99V#xȍ9xz^HVaOŖ۱X-圧Eprʪ9uI4L*#5+r '4Fef5:j|lDC|rv0 #cYz& &ƭ#揟oREM+|VVVOJ%Xlh<׌jHeR 9u +;qo{QYeL$kڊjf>ݣQ!GQ$.&uF^"Պ&s&"IQx{BVѨBTPțv&-g9z Ee Za6[0̘fL&3B)ڦI'0>ӿl%aHlޜm d!H|_'(.)ckPj:x\;>b-$zH~C.)j}gS*驅DGG2yµ ܓ^$K`jl\NhI?%%{Th L"} !3/ yK*sTux{)UZ~ 1+IuMnq>'3Ry阐̂U蜘vs?ד]KwcSWflэ_}K4**wfrrԬ4b[;uF/LQg#%U݊V'/`Lhd1l1s2#W/GcԲ 7GXMyfЮI^>ovxGb}[Y}tϺWW/1}M<8n}t"SS\QRs($̟$/dуoԃp):8Z-HeSKX@?ڗ 4RM6Q3u>}o޵#7ݏAaXXg#7ɡ#E7!g*-|!iWzLQoxP';z:58 ( YR">x,>Y~V#hW?$`Ff.}tEQ4 J((,S61Џ2Wk4#72"7EłNoC.قb&جz޿0&2)A@zj3s WT0-L&Nj`ÂiFP||4d&}maDZ=gsL:%5Y(r 9@qI9(Ҧu,c &ul$3w3 B@cC'm6":J&mbC+`F`0M D*T*44{F 8;49W[e Nc(+'$<F c[Ѧu4Fg^w ql}?ƀ_ Y?%cԵ c LZ~D92|}),c@>bßC&{^!-?s)nΨg̟߅L*˗Ҫ2:jߨnL*e] L~s`*#sP6KD"<]]h^XxmSnSt^cچ ;_L*jM?uYsCᇔ݊A]яx9W<;%5+Wf=űEJÖۉ g8ՂR&2 gO:LQ]D >R+U* W@Z9Cb7zM#>2-b'V:}:F++׃i9ys0Mv=R|:lS^k:/\K9 nfҲ Xɘx[i*05% AG*` &sJV"C8߮hQU"=/nJjC"P(䨔JR ՊVgo+f߁ ׿xiT$G.!zѨI5Ӏb&n\.p&^r FϤo(6hG]ͪ"j?+2p咙ǍG+@XhE+JZeѽ fҷcmV܌Ji>\on@@/_GQY e#T()e3OIr9~6ѭY[z&us{%eJ_&=պjcRRYHyG2t"~|KLXtmWUC&Ư51>Bɏ;l1#ɛ $#nH3"(Rx)ζ`up M_^5*gMk3g־#*/^-& K>ͺ VrĻ~|o# A5ZӢs\}jIjcWC/?CXn*L֓(#ͳvy:Qך4Gc<|Oj?gk{OȨ1xI]@81OԡR6K ,z aӎ3`j(WP* j%lۍG/ܮ=RTUx|\ku-s׎Q4I97x?/PPZȬoC*ڽOat.j~ [l瑛cxv]L>&-C Lq):Ӯ <>u.Q!,E"/ u1zρ><8nv_W2 JJU=MhՋʍDbX0 Љ駉 ѷaV6!J|F'рj]uΣ̟~ٽ m[b>{bJ4A g|K,Hnk.ʭǺ sK9vTf*Y9jŠ.},|4yV]bO^wLwKH:KDŽd %1U۾Cg֢3 @"ґտ@h@Gu-#?weO~S҂>~=10qs%lP47TPprp]R5o 1I=k^>'[oB^6,4('m#*jM =v~Ƚu99 aIz1 Y`Zst.TsJIe)[YIJr[Vvϴa7Qn,e;FOm-k^^M ,CZ־i{O?[ImP).'YJ3>5$@VQ.C geJ sqt7=uMLͯEgԻ|fVZc($='F3OȟGoH$S_V,{|뼗bFEE=[+_Mi+tkӅ}ʬgtxkM6 ^0WxޗȰnCbWo팞vÆDք 'fy%ϡFM 4!ȤZJiX.Hj4\YiY%QT"A`Y9xy{ uvqx&Z=Q 4b_chH E:cf Jdldtb[kR`Z)*8*JQA$ǧѷc]6{K}4l`0U9‘Lp"3A":]xɉ(b͓K^D"HUaؘِ92$V316zЭ6+O.yNsk牤\#,2A"]O洍y;ة5u@Xݛ Ȕ؄:^hV]TP54ҠQ3w)]<[uQh,KG\3ıMgP2Ux']H~q yb&B}ƨi|t'O;^m;1aQĄ^L-#&-s5B':%sgE"HD^&xe>{|K~ƝcnsM"Hc(_o[mkHmKۖ*e7x\Mavc¢qtSmhj{.7)}c4 sX-X;!I,f\ǧu;LhONQ.g0s¾`K_Z@V[yCiG9zcOT;P%Wb0Q+UZ@l ]DR>6+%8b+;#z ޼{jUxm=_/[)z O^DYXc1-Nsiٹ7,"sJ36R˹ݹ"Uрhk|w/(|*~y!{e}6RIFAg"zM zm U}bNOV댘x{kׇ}ͨlT^MHd...#$8kn),,bAV⬽ /ѣ&d,K ~H$.f??Τe8?TJ%V?Kzh2Bj/Aa>" ɍi T|J KvMd¾̋+WȂ|pIOH%[th ^ Ii3DrR H钘µ=q J/Z͆fs)WĔ{M3ydRFLlhׯco~޵f< ROQVUS4qHxԉuC2לg ,8w%/C._Wǟ/o q1QV{1g,^_˞߻wnp{ɌT?e0xq/<DbV̙8f~gmKH$̿IpTh+֦Sm=cgp.<_;<$O[Q]K t, J[[_/iPPZdDGђytHHfЉΟƛu;֓SGrl[2tךbs'}lC[p4%HvODD2@wR]f$Ŷtkۥ:ABv]{ e$vaZ-#lw~t΄Y#nbڰITZ0㥕cxMLӼi'"g2F* JHڛ_xgR f[EWzp}&8|m;hi1y =wzmzKgٵmMf#JmV9DGagE0Z%Nsڱ>-<}>]M@l4GԛEK犊K  ?=LEe5%ԗUw8gMqE~fr54/ yR@J] j0fPEPdQ(Ѡ'+P+r|en F%E Ar$0RSRYʯ;7>-mb[D_({:\78{P4jM>]c,8'=ߎ HXL6m= 48iXF4TZGܧb4\n#hgto%Qs%Of Vw1\J Lj#]]VJPn2IDZ?ul]D"A^TÖA L&$@^I>wCudƯ '>"O|W?9RԮM؈zнmZ+j2:ο s-^;K 6ccsr?xR^|L*E.LaYY[#z a= w%"r7*?/_K~ZgՄL`ϙ;n<{LJjyWyD($(|{s=2)Hbt+2 dgT Ndz^f!À :tn.6K3q tMtŎU<}[+%Lp&w <8^Lj.$֐ !ΒP8$GXY5o$NFgP quK. k'9W2!iuR''T]38ԩn4ϰN{'mCk۸ +X*M(/*I tn~׿z{ mʢJWMx`%]]0 ( pg?`}=dF*Aa{=sqC:%1HJsإOMo VvRY &AB_ڬ'Dj׍z˽'biYiU`xudf1g"BC`zhD"a%Cp-0(Ć @?+)/'2(6H(@˰^y7~XV᱕ϳV[jmʼn- "G{zx-j_o)sfE $Tgb+#pw 8ѱb6euS9rry?O;o?A`ֽ<[ ԁ7^ }fYhӾ Hb3ArI!i`C\ztL-Aq8gOA"A.616&FUv]TPl k'gZǢv~sA"m{0M\ӻ xA3V&Y4M/,"I u0dVcY:2 }RktNsJ[b#ػ(:`FEzx)h܇]FI 6a=.>% _w\*G.Uzfmr{1g,fdD.&Eo4 1Y) ٹkAyy%yhZ:mtԝ_YoϓȈC3a9ccFnq>-ã}^RYË&*$o_\=U$Dc2}b,>!"kp?!d13m 挟U .Shm,X>U*{ϭDDpma,\Imܣm[3`<~]{ =GrƝ l>xmgwa{;LT =N:sj#6s+W5;x.zҡdnk\z^P(FEש}4NSC,4 NŹms^)(o$lA>wt?κ?a|1tϽ'P[si89J˰(ZE;t7yvMۺyh^VGv{CB! @"Π@2y)Cg;̝v61 vP_ 2]+)GR^[Mx.~Cr9g4y}g.[4v/?JBG "=̼9:whH/D ۧ˿/}d.335'%R)]̦^nw܈ԅ`DRC<ɼnbʳd;Lϻoqz|enGNIdt-c"3sEkvlZHt`^ڱC:!p-@L 87oKWJJ+ҖPBAD +׸R)LP\&XH&C>_? *Tj믘_ df*bzuBojG uiT7J,"+7(T몛{W).7`}H%2 ZtJ^8.2wQMf 3qи&%kMFAV=]] 9T*RWJ䉏^^$Wyo IDAT};ywkxm̅+BT'=Eed|!:?#U7~_cߔwxൠW\ȇ2ϟˀOvF}z!0K9R?^hg]59 J-ؕAF2ԷA\E}*js6lB{?xSrG0. \ZYvI>5z9(ՊBt'Ī-߱d{n-׭j-+>fZEN$c-EhC"7qg:G((F֛ZmV6hD/ơ,6jLKE nDP3jjIis?ڛђ'oy^'eV{7\zEgbY9AYU9>_gkټ7z- V-iՊPuqer$R+2)*$Қ=:GZCf᷋o~'D&%prq?L[VIα4N]YYNf@9]y]:.MGQ]ixDSj5fj#2~g-\XEog2{ﺉrBC),*6c6 U?<_".NoDRr;M`b?."apRcLYSL0&ypgjJl5[tD7zB%uJS">=fyWȊoѨUkORzHN,0}ek\?VAd4LnfX-?ܾs4fKJacՕfTUwˑ#fZDт*2y0+a6F^I*^{^G74%3\ȫ?,3 kqGp_.-6 n8OWU3LBQ|wd%h׎nm:l=vfRF4?/8s9), t[k{SmuLL.>~Zhw!<1is>6цZd1Sõ_RL&U8]l,H˺酗JMaNE5Kk+`MVaӐ{TAddOٰs ZFタ 0p6;"3 CT _ &~ ӟKHDwIqwDBDxzNIxbeKӔX# 9A=x;j(|BqMUl(AAoݷ) Zu"^YgүlJȺkgo /13#c2d24A(֝DuOA:q}5 ilFyȮ{Z]ODlt3IoX(̡~N=O=y *Xb=ozĩSҶdVr x]fﺤO!](ub.t&rΕ{RK\}r&n]iGp?jJr@?ZF(T-mlk‚I-Pm {iDi!Z e,1W&ڸ@A{((H٬D2aصz{s]\YMTA8zװF7s$Y9XrFɭɌT_L2<ޓx勷YaOP^rJcM-V wYcBkwh62/ H{_&$_vobɏ((`2`ZkS^L6=4i^&#cޭNãit;~ڵYiuN+)`7xax{\y5Q ][O>z+_'Յوथ;m?ie4tu6x/m']Oo[j#!1C3I*#jAkf J y};gmR߳^Ij5a Fmҋںr"٘)d |1 VE7N:X{(WWM9D45,rmRj 3FOmSQ]dbF?Oh]ZdF*"% 7M߾ٽ ̹fhbQ]2MD{T[SIJWU| ׎ջ_h-ﻕeڝOqu2yF4Zœ7Λ5:g*uHR\C꯿ݳ3 lFp mXQ\+o˿Xnj[]kL3?r];8]u  5?u:=~94y2*U ;|1e H.Q`Ti$#=ϭҒz03g}*fmjD"IIe@/⢣)*.42CoɳNLcC/ߩc`@u%cOUͮ{ytL,6TtDMΡZ*&kefEP)Q |+6< 6cMh/ӬJM˰yz{.<7 0ԍ`?q2`w( m軏 oyIAq- ?\ؾ!-#sz}JBd,} {Y=Wo7zD݇2`ٜ W?oD͹_mf`'SGe/| *j&0ip5q-# n{)kh@[G1&"zQ̭Ӯsf{Owr 1Vr7s+o=6QjeoXx߫LqySq7W-eX_hJΠ5KyeֳĆ8!.Χ}\;bœ{OƆpؙtIUT5EUX+9LB6l7In,0Yj˥ ؐ"B N]VTp>/ )(-%h,?bHf(AR8HDX6o0l-t?NbEgu$M`ރӮM`_o02W0K |2P3R Yhw2lDo?3!{&OIyS9:&ҷeԇxht }.,4W_|p®%H$$u u@z6VDZ)1MX,ZOfBT Hزm7hAWfKmO΃=fQD.Ѩɤu2 N~dJ`bbZ1 F::΀djaEf3Z+())Go0bٰl{ԘDеs~Ѩ=ԴZ=&>GɣAVOږ?َFMaQ)n/8l^l y-\-;vnv:-eĆ +2;30m|RTĄGs(Jk _a 7}43GŰ^^톟؛?I6τ mZ}oxZOB(m(xO \]=U WDղ* ˊ8pLg>O_ &SV9²V=+a^ ʶrkyvG>#<($K=,"C!Qoq[r|/7,'G:떆Ffy>۫st'/{0qSȐHE$ F:%7' ȑӅº2,>?6rmWryYd2CBioN2Y޷LuTw!@Ƒ۱"A&ẑ'> [C@~q.|n ga@|| F {O/-cN߻Iggf鰻]>FpVM[@EAQ#tS2lH_xK6o3moLxow.Dz3}߬̉CZ"G`Yn]tEQx8qZˁer`۱l(T =8 ?9NAڴ<}ܲIp8j8Nͮ,uLeYCYAԀhRT*AѪ4 &:ZDEԂZG1sH!a&z-*Q jhтBATd uzm8ȲAɒ h4AαFTTTr -^U_ß7f_ES7`6mمh {E" 55v7>_zmn&"K Wd9i xttA?Z[GaZhNh>~o>Tg&.K2vyYwk"b%PjVg_ZȒ/_ !..Id4f RU%Y"~fDHٳNqyg1V!$Kkvo$BqEI5!WW4^mO3s71x #r3XT~M3w_x+6w1FQVǮ~͠9C&لlVwL4m|DKT例ngƈ|oP]NZ-'|xذjKIecVu QjȔW귵3c".H(nNyǟ|jr 8tw\|!z]t痞aM53 ?:*тT+77~tCNVوBAVg(d~;=^G^?L{0;z5z%LBQw| Ka fŢFVЉa:~/﹖#W[rgr2'T+i^>swϤ?/3fEhl"{;Ϯ[+͓D?8~"7(OǡWyU1N\8qge@45p-߬\aAk5uhE- 8\yŤ$'pi XDEPVAGO 6|-N B$dDQL+0 Al{n9UUyڬCdAeJJ*()[U!*׍䞇_"}o&t킳JT\psx_'ʙ9{4[>@mjʥyn8|ibh 6KX :NC)iгo@3ӁuKE#H=m_䏯g<0CQ@r b6`GK\D /\ϡ#h5zvC231𸱰;? _N}HDXHAn3!kC(|G^3|g=@|d,vo"9. !TdhWcj1}_k=GhgؑO}؎_|zOd,&kn, 5qWEL;Ul'ϞDljФ"/ JD:E.]x6vf KIN\gyM:¢GU>/_s%q-5O8v+99U*Kɡc<컼M7]¤=0Ux:F ϧk DWu9o:{ZƀKz,TW[}w81Nt(*Ņ CSb$Z]<ì\4vd'4N:sޭͶvmk J_:JDA䬡8knk+,&%1zq"/Cǰجh4$[CUVd*,dBE‚BѨ5?n C((m8ӏRŪskZ~s <1?nZOȴa $O.xt9{ Oh,؝vN;$)*+ rj Fnw={~ZfZ/%G?h (| IDAT[vgy"o^g(a_xZ SW9b?AƠZS1vk nl-Pt)[&6<Y;umbۯlܷdVRnva6xIԕBMl;p -E'3gX̜:pMMmE 33FdȊ[nd_&'cշo~OauɽV&TÕ3.nUaN݋ = 1s4eU\ZyeڰɼCE掛QolwvCnihJ%dEF֠(JU'qͬ˙3lܪ{7ŽݬxڻoAY!Kx:0C@D}q>{?[Ɖ-!Ʀ4H(JHSiGZj]Nh)L#Qh(g9#BX}'$vF&C'OYcӆMnwf JMxpX_y%#pDJ ܉92Oޤ[Gc㴟f^,^jG8ݮ"lv$=`l0{d 3S2nI"13 "5Fd0")R8ϥO\WVQk5rbw^2Of݆e,fO(p[uxa1/>x7x7zG~{U JIAiITl:(Hfn{i; sbB "˔qrO&G؋,t۝C0tTaSKI9e9S|<\r qXlB00f Y}FɉjviiQ 7brT5uL6F{=*vѫh:q9G7~euƽtMipNY&W7{=4N (lz(B5k\Z uO+4YC?v?> 4H1SC qcm zk>g9#@j>qy& "^|;( |HgnҒ{*0I~ifsOs7s~s,H-")"˔yTNݙ~C+'&goNSxcU2 fsPk5x}dPj}i>Aʦq<]\AR08$Qm_D/ç#_gϡ<_aNCPڰhܷE6 3ϞDIH/`SQ=o:'o9{-ߋ~z;%^ݓ xJJgY\.77beƴq+Ǔw̟3N$2jUG J !Dxf!fۍvM"$64";M ׍P-vI5fq?euYξ~*+&efKUsQCyH5V'ElZy%J̪/v1jZ/B ϰL-kDh,}I?Dtzd|f#+9a;.VcXJqTy@HB 6?o믙@bZ߿W`"!aFՖ/&&o8Bۈ~oF_9^SGqt&E& S/fP ͂;8]Nv+EfvKn~ڼҨql%."mv7' Oo<ˌL 7/}AblV?sgnCVHK" IYˌ7g6d>JZvG\gPc] }!H4 Ὧ ,4JT10n Z(30?=JeRcc+լֆ(4msFj W' |l`pwWkiퟤz"A"bT꾢bb¢A_\Ë߼aS/Ј>__Pʪ<M} p98"8moFDHO~㩰TkHv>/!K^OEdj߶DΟɔY<̡޲c9;w2.Apњ3>n:y$UVy3ISt֟뷓 &-$[JSr'64DAF@pP))-o-xy7n7{\.>λz9FT3@7ZVq"a#;QmG$̮ Шռ7}`:/ݷ1tBvE nq bQߗ~,26{%.3jN q`F 0s g%0ew獘Ø>ET)4:2jF jTIP:뢺"2?}[01 0GB 1exr sۜ5;7A׸DSl*\r3ӆM9Wd y. FdHDեJQą0{v).ݲulڻէoQy1~ޜNP>Qۘ* *u@>kS TxA-AJ'P;c#OST|7/!Q+'Zb:y)A@_5_WL9ՋҎ6lVckmpx~9#g4Ba~*|@ӯ=>Z_=[)(a9Hq\r3?lZλ߄AcƠ՟: { !K32"{݀ɾ/dTx4wKXq? qEL&9&NӇVanqz^ff_S%Y%:*?q":vb-DV1l4f{5cCD@UQbu!g_FuQ)D[>KI9[>YƑm?14Iןy\)$ $..e$j[?L=廟b1Xtŧ=g2R@;?Vhb'dDeN~it>st$?^TJRok(ZѥӉz f /bO Kdx`pN-{p˰&3N+wt 7֣;w6&C~T഻Q占due nW=*jaIߺ\F}ɀ$B+HjFo%?}ZqQ()Ĉ q$H%yە[\^]+m3)o}%vB>2ZJ{b1}+^t;BO$:I@jUu7ͽ (>dS66o3FZV"n";$tFATq]RȻn٨,>ʚF B.~aCYkt'SmzCL`-ETG~-7T~Tz2n +rt[:ѤDnu" mHq3m40hg0+ׯdAmN'KHLĴK^Ǝ,kkm>?}Iϭ̚q~AdCXc tܣ&`==KW;Qbr) yn ]bgn're|ό6Qz9" Nh2PQz佯kH6US#|Wld3& ˲v1Cطt9}/ZѤ$O_7ptV,t=gwB ܧZr!ٸiNѨIL!d9 ݣP8ؐdXEO*9FW˯EWsgB54;N?DU.\. oGa5H. KB2 4/QZҡ>F@'?I S;4i$MZЋyEX 3,ฤ z$$]oAXz e\iEhjD#(es:?jё9h= tN Sߦ1[J(ȃXh]B{P}9*`Ȱ41Mt!>-$T&kn^  }Sr˘>h2봀=74TE=j6rbluE(tjɱPd87\dңUWRnkv |_Ղ <~"QrsߵUޔp&?5s}c},8g|/V}Έ޾˦k\bO[Vb6 &iy~k 5Gy}ٖU{^ ƓZ36WhnDkLit1&SA%Z֒ktsOɧbeZ>p3@:qǝx'ϻY]SSRy`V%m3o9guQN?P^8o+1˂S 8bDRs zb@VϘ#ej?AEd06ɨ "GsK޿#c1bfO~ApݨU*dG3yfpOQ>]ז~; 2gw?`وm6 *wMsX0__#8մ8U$KޣN}72喋Hn5(@uI9&X ηc/Le?Iߤ>\8|m!xFH2d޿?\1}!/|:8Ū?V+<q1 6 JAVD+Kj@_ %Ui<uJyu!`wVny* uGS:ΈAT+=ť H3m^?u#fobѣzzf}*Ee|ַSaiQaկ1khc&gݲ"+^xIBzwk4vlj%pыFVcذW(:} %ٹetrw^9&7n~ ;_q\'Fq&vuPN{z nK^'kmw42RCB9\QknRQg8H:Y$O٢yErX.u]GVP6 OwwZӯ=p:rtEZ$}vLQ&v8.bI 6S(huhLT[(I2JlE1EdES4RŜ+Gw d t]or9!OEA-;\yIUOV@VN/J ‘-@XAĒ.CwJwݡ$Iuc{MyEB:v q!s? lVH i &OT"FLEKAq _,cgHL>jMKTN߸GŴϛz4ZՋè"AȞç+/v͞#-O͐^}6"CC1=yGr+zUh5:"=bǬ\nvBU鬡4"C[o/~AY,?I4FeUHޡ n!.ѝ'\4΢w$w{O0 7iMhHhZ>M͉@(|е.Oh) T/vUZ]ZQr/6ڙW <#gZ:~OFElBf{>;Gw ҙ=fϠmJk/&IOۖ4arJeG9RgpjFϴ"+)l0VijLiEJwb^ `q"H޾%'K^ŴޞsUV+w2s&&YaGrN1ePj[ LEx4:n7vž#GtZ [ZtZ%tl4RxXlf2*L]uPx$fOjao??tEљ$JI< &p!I8wnYF&<ʌFbdy{ Y'3{{g1bhF}W>a4wd\p2nع$ ZM{'5> 3VgweNAdj6 JhZXuH3  m qYcM끪6` Ȩt ˏ琜^9' v8!^7!:'E!i-/+%Njp4x~_AY%VZ T:ܖzFƀ 5݂ @VT_Td8Sn WDYPY\ϽQA40!;]hFʎ ('Y}cX*<>z=nYE⪬@5 AeT"jHVEO`**Gc_jGF- DH.&az+:O"dr3zĀvEQ`L<EQ(+d#dfeG6's()-ڊ-yj 69ĀH؝uDHPxBPC<K5Ʉ?N#'@R7QU"&21s:,2'AdǸ!tcv9uLϕp;hjAm6obε9Nxp{`t'$x`R.yWz\}e=NǢz-7µ.+D1|Ddʐ ν"n}sǝZj=nUjچ+2{?0y*/.iКc#! LՆuN`k`tŽP9jkO׶֫GmOKiz3/2a~7޻+v (@cm >syhf*2bZSlp8AgۿWfEOO7Y(9.uGpʎ G9vO dqKhJ+ѼJ%[ZDdZ6+aXrL:JK+ {S*.vV(&ſ︒"_v5 Y] ŅtrKyPN g*!: waօcZI8V~scd셞}q @aߚz)-1 mVAҕAI]]p0,6;ؑNe^> @Dx(a!,IpKnKv#2N ÉfjCd::oq.`P 9[7ڕ0lG[I'DxAOc?c 9?<:j]I߃IuO) D&uȶtmz Ò_tVZÑc7l6kϷn40e/G~eY8c現HcĸnNJ-IYuӆ# ӆMbÞ,ۼKϺ_ZЁSddzӂ+7`N;AOȾcXW=ߋ+Jxgx11M-}ƷڋpZg8-Ю! &M5jܵ "vL~3rFm"DΥ,tQWιgѪ/ؼwO絁. }n] l$OЏ)*,Bʫˉ lS>mB́h; &z JYuĄGqyrȊҵߓq" ˁ$K(S/oroOcoQgj:~jjϞYّiF⣢sj5HAKM.&,ĎذkgS(?L ̿P8d2e3)FҨ{iƄN%g\H Nn%% :s90-MI"uHTZ5;l=פe1tɥZ1iSZld^\KϞÊիXj%>LX qNd^2۟..-3'_h^,dxʩ z6,FXq(Ycxt445cp)I<yyu xO<'$DEеc}m҈l6jNa9H)XNܨA<8ofQ봬{w3n!;|"%nk6c1I?ʼbcԗUa*`4~Λx>ȲA] krBE:](GMNbTU2 5½ČCH;{E?V_t3xUUbX 2lBmj\ȱԜLxASf-k)ʹ`D @_|B'*ۃ|4YSS_lxF͠8b%3&<GZ]cy%E>SO'oSTKfn.مTWS^Rm8FE|J:)q]+IqeϿr5W1zP[_OIe9eWBE] X,J@&m-%IBנP?؋H?uiSޒ/X,,6ŋ ;}<&c7jy^2;zoO҇> Y)8t߭eÒ6w2FҨQ/a94lIBwJEtmm',0cҹq\wsw+=n&$*(ZOTpAYVߢ@ꉢRx~sk=dHwnuۏJ<6SZg|p/edobl+)F$Gp wrd8oػ?|HZ;&eG[SwDFGL3qJA_.6E-hóeB:Pґ:>%NN&m+W[:dR;lvި{n~.ܿWLxH8;H `\hB@qݼ+zujeźoϹf<~vo(;_U]mج݉+*#Bm!>&RZE2~SOTp$%T?~s/cTTTqm[ aD\T^O>Ó=̫ˋcڞa} GI2sgبph]#1SFQ,eV1:lV+*-ne)8|YdR]XJmi%^OBl}1Y̔Vc1YBRD,q3_Yv+Մ_ѫݳ}ogV["*2Ww1dY&e`z&m|̘ {waTE28Lvpﮫz: Q0l*a(MŹnDU%6xoDxck[H;6~ɸ[t 4$Ɛs%:>$ΟOa?ajocԱx5|/T?Fu;2%d >oф'xzx ذ`XbY0M75PXCeM%ZEei-&Նl&lVͦXYel6lk&pYel ,<(,*Yë/ﲌI˅SB0[V BSzJ-*z&=1,%- M&}6?ZբJj`Zw q0u4AqvW{ID N+g0HGV;|':4}B%3m"ɛo# cYmLjk-*S5n}6'0VK}cD08)_XjBr-o3F˜I]z6BfQ65<}ç\__.z^hXXe0e g$^,|[s17̿'sڱ|{3wu9S`Gh\C{YtpP#;N-\e1Rrc~C$Cb=mV" Z~Y/>~zg!av!7N}k -:TjͳGa||Gff< c$ NNכp0fS:L fg\0͟Я[9 dP1u&}ϙغDzh2[:ܾ27JFn.[㊇'."9כ9]B\A!tdTACseEdd(?l^.iHqryJ`í\υS/^5+x0f(_0=#W%O3gtV;5N!n_W;S:ԝnGGt"eEzƝraZާ#\{]t|s [a8ߺۯ{$KIf|>3n:?%_<u1 &,k)oY6đXe#j7\CzZAY!WU*69>{?|$I\{<NJտfC%IȲ6-8IyU5:(<m)]Rt:-cc4"hTh|}'tDH0eըU*ӒMjwuAef' GQSU@͢h&IDLOÙ17N-x)LWrhUX6Ԓrœ*b"lŏyxt=vfyh wr%ƍpkOs*AcK9IF(>_s>¯+ I+APE£{WUPѰh1G45wq<:tZ MjPЀdBZc۟7m xyN66.+M,gsI_="1vgw){&Ul6R%N2E-x*ﲬ饸M /4e[^t~)gf!7sLeju`^.h zcg,'<0o~'/o':[PGMC :on_:?}JRdFLr-8^=KoрanN{)n=te:1{=[h= tsQ}L֡~?!g_S*LX&iuM?o֍H2V]L\]wY\4mAoht͗X 7w sr{D,+ #b056՝:zxo[nzO' 2kh6v 245h͊@ 0x,[0jS@dI%Q.b/0djnǑj/VoUkxr83oO `y DjL6,SYWV~ݴZjjm2MfM&D$iAFdYʂR{+OHϣCXwi064JTjfH`veѤ H- g_G*Fc6#ՆF]u^mӓy>e0C}7o&1.ϟ ?-,5uT*j[%F^'+-ɐ4@a$eg( F^*H6"NC[' tlgWR[(c2I՘ 0t0[wx1!赔#N桭.5o3Mݯ[wƝXl+C+lKI*$E, UxAuxE9YJ,MsN*qb0h /4lyDŕ(CLa}=h (<#%(ecsۧZYM[[QPXBjr'3 %P||PI;J國nU w0c,CfODMPϲ),|V:J$?|_.Ge:ucŢ 4f=|h0+/ل8eJ$$+K;PZ6r'JƧ_}^e֝/;.U[AZbA_y5i ݋%+ LE˩@|};dA,&,Ղ^2pmemYY&`Hc] Td94ʂR2%gQ dblh'Џ.HOI>B%uVt\NpQi0`/4Z%^ ˾-Y̻nh=9;d8]ӺFҴ f9J,@PI*?P mbxHHɞaӪqT/p4hnRX5Kdla]RY6OZP- j DumcN*˝~ju n El$olTjJ^> õCQ8bO,t4^qwf \T @1lC+lP^уx| "p1fNj#HB0n03˓3'0{&|.9yqyVaĀ0e&cZPԝc9@ xڅDp@|[^y!o :s1xcQI\z+|tl). sfƨ),Y1>\y睒`O-<0*.7~`,V+^~W["%@HTKYdDN]ȴV`CMv8coD`,9i%,z F.8Z5s/gν rvSyH~d:<JĆ^9LYu9^^1"yh<Ӈ,,vl5ň$A_$94l%?}4aY(q!\~wB6hţW^ʠT~?G04>Ə7?|({j%Vn_"4]m&f#la8{4 H``j 8v>;9z'%!Ҋjh4GxspqjC$FNNl¢yռ[%^q7R LR9]WKl>uAaL9e(/,e*V?M' E8t 9_bv֖p.ԭ8ystEBܭEgS3՛rv5NzRmhXvAM?o g;6NG (/On^p-:oBi&>33_x#gҳEåfOO^utZ}ilnBA]c]<&cm+'o"6~9De]%Cj-g`L* &o~IJN@QM.{g>]<&"f]#{P޸fauijuуzg gkуSi] [/V:w'k~v#yg̯$qe)/_^F uZmop)|GS|X,  KmC" fWOM]Zl&$5oY5z:%TW#D\XtNy{ rsa$yɭ\ '*JX4LdYfmL9S3v:eHe3T֟Qc(g̐aRf߷n%:]oeEwK>_bhz_P#Zbp,W^E#PWW/ɉ^Z/_N*B(Ȯp l~j>T&$(>+SAA xzU+V+0l][ah\^:LvP6力nV'0(D_-* 8v<_u9s]V!5%_J騡Mڶ3fOuc5@%n6WiPsu-1/9b9TVf$)c|K!@0(}ݕ+a@P+/Vk JM&Tb ju=]ZM LJ$G3||:^su p l۾O^qgң>,|=NAB\˖WX|J"$} ʀLߐgWC;՝#V$$DGsʧmVZfgZIfqlڻ\3-QuI$傲ZoӆM=Ǯ~/}o׳p|ԣ@?n]}ϻieZË7?ɺqݵ&cSUܮ,)2G,^67/ԘӾw{esŷb#2^&N&mD LEڹ@$aSxHn+\ŋk= L EMa:qPuM>;VF>6tNnZ~y>%2Oz˞ vXz亮gKz)/-O_F x 蓼G)tޤLN^Yqa1n~ˢ]PQiQi ѥBF!*$RNF%LNH`\x&K~W=!̞?^͠H*H&Y,D#O/f! ȓ@?T**F|<}Y+u籽<W<55N4supmh6_V@mLjomGLJ jIJ wVb~r?|-7y7y{=sBxFhU\USݮ h 3B:'/FP^p8G5ӁCaPYqh/@V+JD' ( L2RI(rNIf`j|>o&˜5s~İ)'2cf &ZqʪolFB縤h8 r]^iux0t:-2WUu5P[똌ʳ `W X]˷gAѪJ"X qnՆf;be!N7Ғ IDAT̬Y'hUIӇ3>}8|/\z= 2fxx5"ѣq2__Ɲ^hs(.)營~r.MC:30{k $Ҝln~4Óm6dVwz?{s&6"ⴥu24nhTj&:FIE9^^qL$'܇־qwXt慌Hʰ=.$$d\jr}ӫ{"yg9uVF2$aPemd:~br?9ٷ(߸n΀\\Sz:z<14Z] |Wls;ىsw\mncYZ {׆r)M?6T.{-.4ng)'Oߙ5z[m65ʏYk=w]rkbfu $/{ @HH%=A)T2_P^Lx@8ο+_Giye[\%~Ѭ^uw(8'FLAth$^!/ōjJ8m9w_ZnAo>x}b)$MJ((&0Z]SSIP` )ጃEm <1!u{מ@-lr5-r4ctuIW@e YhVy^`lP0kxª3%_]-)B5 ୒@V@u%)Ix}YJVXjaP?~ =`fKt^֎l2&<(} 6 cO]Kⓛ%!>0Qa}UK[AWw:;UُBR6zϿCGSk (Թ6+/*~xWH_/3*eD$GrLc@g| cߖ6D`TSn%}(ƏJeyy$PUU˭w=͡'y{8I}ZJ "8!`Wi m <|xܬL x`Q PU)aHNeUFۧQaR 5h#&)3  b 量A)޶T3.8f" e ˫XffuLMGnlPf@ĠLLl%;|BoiTƃ +0ŋށ$70ڥVJfgrP%%y*8PyQ픬4lY}8s x"2"HbDR'm39O_ιsr u_15%9aCSywɗvNuZry``1֟5+!: ,RQ:[U*oafBGfhj `X좄ُ9vL;gd4-}d2rI*~~ IJ&96VF.;ILX8$("c{.yR^@Eu5Ony+>SG^z`Qpii٨m#(SNGYl9'?~?/_nzw7I,V+0w^x E-̃K(Μ7Os8<ⴀx{p ?W=ոAAFs6-Y]Vp;ɭRuvxw +z߆ kQv>~)Q};ο5iq7b7,yӱUӟ!}ͦmT p$۔Hw;x՗l; ۭ:k PAo&^u= \7Jru -#Z{ॡT @9Y)?'3O 潻$%*wfT*>>;Atb;P_KT7q(6lwu[:Z5l\*:rr Ge^ %9v"]Pj s2i46w@qmeyِs6*ـ']W;ZIرb2hgnۀd!`!=s"_}QjF}:3_WҲJ~yz7lLʀN!Q]Rǽ/.0TƐJͬ묪ݺ0>+`Sc.N^ hؚ8E=;/NigC`@sN`tSF` ?v2iH29z#YTTVBRb ɉ1$nS$ۯ}k/ӽxgN;pj%+D!$Xt7SM侷h!0Hakج({ "Fc <س )8 Lj'Rloh:bDŽq޴l6>kʐ$ _//F L#9&\'zo)NO ZF fYitTTbYQNϑ٠ƾ`1K͆}ydӄq7)b!o|>̟p12OwW{Jnqغ`#\C]>+ JGq.[nͧ>m/x!95E/):c.]稷AɹzOP> }Uo9'=W+m l¿[U>}9tSwϬkɈRMx&CL}K}N΄!/"¹sz .޿ $I $Uo+]g_oKߣ>|̹O\ .P%l4rw,z)vWWӒ= Wg#-p}>r)9 ݼ/~;ǧWXqۗGZ@j7,?}6~|M_.i"zt{!VEGiK"˘eR;oGJ-mTH*Y%#ezgQބ]d7ᶲ [VNi;y6r%) a!가n\uǪ#|#c*>r0YIsg[)fk#Aa457bX Tqc5̻jjSg$.Ը(y__o_xyN[Oob2GRb;~ZE'p*`;d\۹E/ RT;"(;Z]br|E,VZXz=ۉ+n 뷲1$PXL@B,e&36=M̲Z9Ƴi˞/ u<PĽDun8wĮ͇NJ?v@Ov*~Dtf47+76(c0_')U jսXԨMZB" g ỏ;;Diл'm`}}#*󪍼L?/=甘=Y{"w&piq̟;n{~eQXXBy9ucZ]޼% zۼ`JjJŇ3H 5v8^kX7 ,;\}B>Qvrqz}~w}5`7RsL砦eS.{ٹqM?ov 5OEݲSkp˨oz1zH~)A|Ϸ?% ',fqNP;C%0YvKԩC VN5d$RE׍tpl6Ԓ{%7!aO՜f:ҹ*I,QQVc'P[Si9sZ:Wg$Df ZKj>\ƣwIokkwd/^=@FF6jA(V}YT B)q!2Va"ܢu WZ0l6{㐐9E0Ο(.w!$3íٓ @Rt3e]= Y'NFN.*VFiԄCBdC:ȐPGZ?Vˤ#;ojsU54:Kr؛,y-><3%?.#01F/ O/{c㨮ޙm*޻lɽ7 666tH15t@$@(@M3`1,7]Z*ۋ,OyX;3wn{9|ԟQ7d@˱uuzZ4MF{g$kc M K-dє}(է^ҿue']HZrR/ܚ?z-CSesPGK|_Oѵ'p v^k؏L *ȵC"zgȆAwuA8hv6jrrf4hydUQ{grޯt;d_Y!B 1:(5;PJ*$3XNːpz0ogi:t`Rr/!'!a\]O䤡ܖSHl>z}#^=?~t‚[9}BT >>sXwVt.>4&O;Σ`JGn;7}7QXr/9K^zN_JUu=Y顽#לv9 ''nZOSHPL8U'_so 9Y!^Lu*ja羽|~=vngG+.g1zJbvE(4ֶPBkK5KYe9z$X9qQZT@QN.I&&c4!4y)@}[ /M-'?yWNfr# bY)]]u]@C5Oo!xٱ)?Ir|2oL.V,=f,>FUTc-⿟ͥQEùrAA|!=egI̟4cm|*yKQ̧8xP`ExYGAȁU"5Cvˊ.ǣovlkH Aұ)|U%lˤaǘ-*_TiO1[,dljލzttܚ!Bh ёZ nKc*BT'َ܅Ѕ- t |]uhwmvGWdNZ zn$ZI@~K}^rګT`N%v뚣q .ƒfP n+D7Ӻ#4p[芎0@ռ&ntnV}mY)W s꩟rZSSgzBWTɅjk۱y϶Bj4*d0Y,&d4Ūj2 (٠oP! )\.47X(=KKM{M qVf ω=[:tBЉ׷>m<#G u?TkYPBq{(.cIyn.fT\ʬ\ڳ#EidH3d^J |5c0H0+=.^Ivj/ IDATBS [lm-!߶K**U+JX)8pdѬ@^n&Ӧ7> 3&PmhdaR^L<$+&﷚*EUU C4Mo]GtZ)/`Ǯh ̟ͭ3GG$uںF:hjj͎F~;i]t É5=x?A`PQ=tu9=r(ƍ 99_~Ϳov]aI^SGbNp^T#sVK6p`r:ǚOz方bb5Qɩ֠b|m2' ǽIˆW|J }\*dfs|=cs'bFg;Édb08,[2w?Vk8ٳ&I&n<̫\|2o(v_2ojjHLUxlh^ݳ'V @4**g ;6mrcHH!)+W#95f1`^/&fRxt]t2tSoƱ_Lav.&;4]Ou6cEIM=. A}7L )J%u[ضo'voeWY9kWCU}5-m[ꮾiddi|u, SgE~ A8 AB-q\ou ug_yHBZ9~挛NVk<UUNs\+w_øbŔhCw8U44wE4Mw8nk lt8McC4aY{Z Vv\4%Vĩ8(P9ۀ6:eMv^sBn"6Qh*huU7a0IyM^c/BT X`1f2 F]‰C ',\jڷܵ{&NSUcO1jLԙꪓj6aTqtMTאQ-s`\8BӼ21QXC[uܠ|2Dl 19DrܡԵv}BP\GFR:;_<գm$&XÊ"r7sv[@z}f5沠𯶶.]sf#dfmMu27`{||GwJ`u秪TINzضI ,蚄)Ir`Wjӗ/P4@Sa~ݣv ESSHMIb1TTְ~V{evӗrqk$7'рd$';w<;.]]tu9q:8]2]BB+3jdnMtupo\ϓyܺL7MesU6WQ:sa}L8 -Òcfkyopyxጰ9|~r gN⁇=sR2N޿N{A !(*(,9vӴq TTPDGg554SǰI#VH|b" W'p2w-e]}|^E7ݯ>dXPnͅ[u7@R|"y9$' NZmS=#<1ɚS>zgtܨBqB{|Bl<&c׮[Z3"3.;,xϲaf;Gr){A5p8iR^uοr&/$數P@?z_{k??<Wf-#޶'M,!r؅7Z. T nd(}PfHLc:r"SvՉ^#(X}}C6pXBrLij`YQ6{̵z.@⡞)_rf&H(,%i aw맥F*ї 4ۭvK1NCt fޝ\||EuHNNsfMSWE*v5l MJIkz:iS:uO|yE1vow6xx =FbU)TAQ;HB?`XCWC|ƚC l y]8+?뚩\5N ':a^n&yl>/>6[y( +|זNv-b13ot2uOkV:uvqycM/p9ڞfM=;]-_J@&k.=_KG|39+_DI|Yi2:7={giq=70mn) jM\ӟ ,.u9mDGv4 M46FALnn&X"n7n{BDY&>mKӱfg0/ E3y9F_w7B{LMZH8^Q | C)`o!0 I2T8 JT.۷H3`)2h-|Dz<^i_9[MT󕛸sKϥo(eȷwGOUˢ3"Syyk29'p۝Dx]EM2ktfitj'gilj$;+Ԍti9)$+X NӥG`0PMFf#jBt/)w7W/G_-w39uBo#<H}"OEYE| yumL9ٻ8ܝ\ՌIgOl@5:.Ak'P)}b$p+jŠ Ρ#üa9u QɠۇѾ;6h6hG 76ܡw[Eoc yCn6X,L>5xCלNJ~;{^z}/cyN[b^|+pXdL[ϡp7nIci16F&s 0K &.3咹,p:3v|\Hɋx~,t9hvɍ9"dT:uN-TTVSk+kta%55c17[ٵ{? g-bHUWMm;nZ9piҷpkav~;tHŅwn! ¡{'7-* Tώ׀  U[Ka2/-H˄R8ǐ߿k.=ɄyQ<̷k VwЁÌg7G6s}OW]C">iÇ|_|.Q@x IO, .jYֱ̚0&봽=5:*Tl%-8.TKHDt2DW1(Xbc0Z̘ccA1q8?@Vn \!H h֓\N9 ;3<˙)sfHN!O ]u's;[sߊ[;~2"2H_&<tE+u͊P'BTV1B T`>zn"\꟨Ldm3mm 4H_9_# &I¶;) odB/ v8a$?}7vCt.\o}u򃇥>.]OBR 'N6|~   fT0^~F=i,jղm2w\Riw}}a-M8F[^\UUjN9j1Ӊioj5 );"drRiil^ΪϿUUP՛cϭi]nU!>>43k2iIV:*+Ck[G/TLJ2DO($n;bN{kfYx<;P*Ƿt3{Z)vmbBX挓?:6~|S'߻5pN- MDvQx<娯K<o}\WA  tUy:)FN~H=~qwEK=:鄂"XC#>6› ڠ {an. 7}[8s"C غoٹ`* G_$߶y]8u >hܲ2;7_52Lw$`050Df$@z6\*I)U쁉0{\_V6),\on+Gc6mvFo/?wDSO^ԫ1 gpCیl ]}.F6@jTF7ka0uR`!!Q'lkןXC|&O?q! ).8lru[k[#aâH* 3k/`Ö/rkVx3HOdBcf˞mT7հeVuM?7)KzzB@|L\SC z ޭXBY:juQUWڽOG$k9̛74۫m׫VsF 7.&9=&L7<ɿA.fBzDȅ@x8uؤN^eydux݆#wK>BGo==@ǜ}t׷}y$ 08h6hG8x 6=[ӻՔAA3U~j k{PPS۾'sF5HK oRROZK=~!~akAupY+4gm !-f 2ǘ5\R`LhmJ%e裫[^d< -;RסN__.'7u_o_yCqC{HVleSS+ifffUƣP)w *Ԓ;Dd}c 6uJ=5>N~A6_>=Ǩ{XiyT.jԍ2TEik̓2/55Huk]Mѻ2`7i`L BȞv;*hTB(" ߞ6h?w;AGQV DE 4`*(D魟]6-M7ago~N yu]tuIKL4#?_/rp8gXs8gL޶+a`(| Kx ?:F 9YT5JpT2^Y)b.[r㏯]c8g*r(ԙЀ;.g]N. fKtl[hAK[-4Fsk+MMs{G. Eaь`@UTTS(}O(R1YGU띯)d$.BJJ%"0 *`!5JFV"k:"PUbbMTi`\?XUxHd=<U"wȉ( ч-+ c=9sh?14^Dz|%m+D6TH=KBهN6h? ;P<( 1NCDPF$pDn+/g"r"HcX4a${f{b6[Hs:8RSHH|~ku IDATZu77[Ȍ偏ޗ9 y-齷}{ioOAijdfCV^(28%M@knرv#2)m px3 Qc,4ST j@A -4̩GOgt0XWu뷐!ה0[xI#k?8$ZLhdŏNWG1yJdssȻ 2[PCBlk37t=ޱe׫h(,:^^wFGPE6}|~qOPB0Gә9}=2 q{q嘌dNc9 =sRjaAzN1dcEG%ǟ~v;+ׯŏ_m_o~oC;2,@hf@N8v.W;&6;[>^CljlbRRY(J  :y ]|'ϻ!/+PX4iGO\WrKO7|1|n𦭧=a9A}LӄQ|R$ijSICk# -44@CG :}k# M466ny@x3N"#+ә]@Jld q&f#tG(*CDm':NDR'=s$3:Cd_OO/xK?,ӅP#! "x =yϩi[@D"0 J Hz?9G>1-Y_9 ѫ,{yש?m(dQ~!?9;WqG6S$fLd gƍlb|naԟBpO9S6u,EN?Z~q (`9g $-)-d#ɚZX<O]|?8>׫k2XXngM)5榥jk:[55-6L[G;X3I)őGrZ,Iyq K#5=x2HN'1)Ř^T_9O YDN!hE$R a=&Xd䭽ZB\a+nD}E7:Z>"BxsT-&1PW?>lh&t …W(pw2M.HGܨ>OPp3O;O GH7Laћxo1u߹ m,=uyVJ'LtqJK xq:]!)^}c%眹_\Sƌ.Y駜Eqv.ͻHoʷ`Rd4M&ٙDvLc % /Aq e/L\V~9F#gPUi#G =$}uW`褐 w?]Katr8iSb̤+=lZg-74s8`wo;q:G૿?GAeI`Us;F/_ʦ16w>fSwr8=Z˭wIVEpMr揯Sg`eR200\挛G= ]{=[spkinP0&ܮ>P5ڶֲaצup'@ZV<9d$0>Ԍ[ILz@O'<F:c?R "9`I9o5zORw/~"<0k#O(XHR5P1q{-BBH*+I"H'(?jseX^(?yNnoC̆)f뤦!hXk6ҫ35uyxt u572as N!+o`[%l6 V`a?nM2ߤ SVgGa(7?P(Q|Hǁlb*7^ª^\F햝:|r= --ĚNh9bH-)ۭΚT9]T9Wiilzy,3"%X3FEGskZ/v:]w@Kr"y&uͷq_SL9D>!Dv&Uq`fve͟%s.ghqAo{x}2 nbu+9Fb{wV. 7}\ٗ(N,|ל_|ESj&Jƛ2vt)F/n7qcJ9˃a5 t便=?%+zmg[7`1YLAs a3`ܐ\I>G ㆎzy}jO* QTGEVvWjXsV~bQyɳ3 U ќ1;.PfdEyoή+lT/Q&k^-D5 >w[e)P@2" _P#|, -)Ot]ox:JzHFMza. >{o%R.|6ΐ/>BpC=0?;`"X?Ah<:A}" sCK.V>ۈ0U‡=ff3mWFdkBZhh'=ƙ:f)bVu/H[" wq/pWS@dgbwx}W p2 8f\}e`4Zen Sedz&L-!˷ê޵J TOSzC{ؓ>WWNJRCdF_T[Yҁ5;kv9G8qlRYfֽ&*Y4uf3\lYGbfc,ĘI4rI4*?dKd,Bek=RϠη?&{hY齷9%/MKTm/gȡ Xh  kni% m rl c&HϿ$ 9ʼc'܀.6]۽oGA,/'D Ξ>˘Q%,[2g8̟rᄱql9| o%f )6CL }r"Z,0jy#IoZ`FW\*I/-(:L6ÿ3mv~sαBu[XfVuw= V̐Q%ƜFQlyƏĥl찳ÎIKe5iŨ@HHɈh@ux7ъ( |Z`1sNwLv{UUy &\щ.a!lUuY}!>ˢOZ.7l%cTg9~ibJGsi[I7[hذꝻ),%s8_P\{HO+ia϶MƵJ%ߪJp:N'4kIsK؞kd=hFA>?4n5*Ec'( 3؟9_A9 !tttm('-⨩·QX5>㱋g5#r̚LQa?9kyD~Di57~/}6g_A5tqE;ۅEkG 4SXKMS- t66b6`%%%7^BJ<# K!%u8ig$aƢ*QC)*)~R'ߑor#TD2/!p!"DpGJ?A{|:Pm|, hZ/4Za{](4hFk3kk)qoaՉp;~hv$Hkpi@VGp` 0j"V"'~a@^Fy=w~ᰁb'z D\0aAoz ~l%; lQ m÷5= u\K.9 uu) L+>P‡$]&EC[We2<`*"=4 -\7TW6J< jxNvq660q¨,j^v9ln%;^&q18Ywu[9H}S %FӺk/Ը16m嘭Xҩt8{n{l4oڅ'YAA\p&ρ@G(^0jVUlu {*&ELB~V*XATEmVEU0 8cѓt^yc%Υ6߶خ鴭L-W4wvW(?$66you2~~z͹¼Cv#!.SGq9=%N<.7[޸!xc& AD\H做J rشe'GM[nNkvͷ>b1t젞 nknO_˔I1 d:JKL_>?}{Wm_l]&jD(UD`-44bP gv8NZkƶ&[hioڛink ـ5)XRӭYȸD23H 1JlhTx 6D{,Ed  $/<+8F/|DG(;X:5y"@c3 xuKxJfrY/$D^ֿzz3u'5.K}Fq7@x40e y܃'aA?L#i>B2&Jg^P`BjAN_58Qoo|q#IBQ@?&r^G"-<|Q$Ơm}Dй*{s>揚707#жAN0Ms tk}ؙz QU;0|xw2,w;%&PZMHς`Ȱ;y}K**@V#8v<&ذVzDmuFa^an]g}vNrG +bcڔL2.;&>l]Uuߓ؉))$!'$RĤ$^SS\,K2nɐ3za=.܀!3 `cQ1FeIVfQkFU6FŻu|cswsB@Uɟ>WSmIIV͞ѷ]͔ICz u{9g"79 Yrt>ؾEke^o.)(B[IJMa],7n K(OZWHOKc|I3rz~q<Э,3 ~P/UU9oY6}+sgn׸=/SmN@GuN\n'mv[ihmFji ۅA5GrJ<ɉ$%&@IQq&2NbbM(Q{zO$?E >tr9zB78] ;-BD "?kSY 3z4BR1b"|~,xlj޿|-k*t<ӂSD B XDѦdЃDOtsm F-=`c p Ozpi'aPQ8`H`A$u(ȇ EOR+nz`(4ny6z}.9>/@mE02wCo Qp =&B3cW9?;B~VبBGN+W~AQa.ӧ;$uxO9]Q2Ƌ1-Cmu'^ a2CI[`TUU@흐"`VN>Y >T! NoPU\aʪ:촹q뙿l~Էdců~.:4wg˶r6|z;:qoINnyYeg`NIBє*e/54DZ+YgU IDATgѲE~ "*@~eVԒ"/Ä,ֿ',d͎tt+` A1bSllƱ[ʶSk/-U8mvr3Ɍ.p@Աc_EZFR8d[wB@n=ɶVKp 3tT""ﴘX<6oz`}UD+';s:{eR8;@yyUy{QYd˖[l:$@dҗ uB @HHB*؉n˶dwi4<*S>#rGSn9s~7O.S_4V\ /o M\ 8 <B~'mo[u6s3C]4&Ud姒DjUyt:Yype mE Yx{RTRv0ar'$- Oh Wx mgY $Zșn 5+NѴhu˄9lDNX $V>(fI-p6 B"B ᡢ{ Shy=zt0Wwj99ƍhJvm/"[%NlisjK )nҠ>VM-sC37xBz 3pZ0Be"Bl䉨)EFDF+v<6V8*nw g^ INw0 vҒX΁Ar+-FG~ﰏ E9 Wհ\3@ᥧ~XyiGd0>5_teh:; &%GXl0,VQɢ.|s?3--E0{f-7^wivt4Mgh:|S-vMo_?iix$m~\\7dd)l喙hGONN+Ƀ;%º)}y#2Jƈ6`㰵)%2FAǣk.P'-ǾǞƵ}RSYFױjiN@WɝVCȩ,#X +6=~EAW_?_.5@{'UVrԪsRA l\W=痱g9"4&hMVt宷ϟ{[o^s핗|?$]4ͭ'@SQP/~xw}iIxC FnEHGS.o;$VZ9<lB=B'}r: ou/6T;FE"]MhS8*uӫV*@Tsm8H贼qW^q(>x\0co% foUҚiBi UN}OdxinYh֘!lOlAEΌ/U/g]JrJ5e$  ?^0^|~?^~0C108Hi{΁A2487?6[UNj^)ٙd;)+Ӫ)1wr`{}> ?4߿= i#Iee)3'Qfհ}nIة1:5s6lqӦќ=}6=;BVjQTd67yiJ5j{9ɤL5|i|m:gD i m.$b\>pu\ x)lBQx.]pGK_hek+`$`ShvsLv=1{4M;,chhi՚׾V.GZsgTȂAo<>eQ5dB%<6m\bf ѥf~r:"XrǏhErw?!ɁP1LWrɐABt!zhm6δM{W]G!{urbA ť048Doo7%KiY1s/B+T\DM~v 4wJCې|NRv׊W7lacgg]PD>sbA8q VUFJ3s\r5$`<Ȫ @yFW%]/).ij&WغK~r}y*Kؙ\C'9Bx pZ,DN p}]3RLV4"K s֞Fn-Ŷ%#udC.'D3jv >HEӛ/eKKa9Fg6c`5:֬oFkbmrjEI:ɳf%QJD0Zk'avm90 4c/lЈ%:,7 \R#HXܬ#ׄ:{>3R3jggx&@]<|s¡ό=qOuu G4з9~?yٶQCiI ??sgh1olWsJ9}̌22F}h;yQ*UdXu|pp/<!' !Pq#Ϝd5~?>` HOOK%%5qܜ,j ml U+qVn+ͧ.'Q{G7[W3spPC!5 e@Z:oWXwPP?^/>ЧzD:))P: TC^{ yoW29z9R( +3=nhLܶ60i#yB5,7Խ#5%c/xt2G n&cIhzRC5G`ƑZ^^N 6Fl- .(yPZ#ʺ+#gחM6sRkQCS#]ؼhqtlzC }tVeV&a]dN XD$VȒQ{hZ5rBEagujG-"8ҩ FE[S;r]9:A9> w!F\EaXd4+Ԡ֗HF#'h#\-h(Jv`(vf'j-JY ^XȥL+=5?ɌP3uo=/ P(\c _cq.maq-/-_?(^?=i /&؏N!ZEJxiH%9y0.|*Pӥz9ghG;sUB%+UeU/̸:~Eŕr ֡a6wwh]Lb)$TLko?'ϐUnϴPzîo̝:cvCzUWPC ԟpXWT`d^Z$kٰux5{FjJ2kV-\6/}gTWWpۮ$'۹)|Gn)*KW37V+sjfϊ\ZQa896Ӝ /c@3gxZw)1tP12kIMټщ^A H{5 X`T%kcXY= - =bp@d),}g a~3O47Ya&QjOQA@y/e]S e}z^nO=p'4 Q2gPOF.B={AEHFӮɠFߌ}FB 5s1xQLh;1(JD.R2j ֶg>#Z,MIv)'d!ge gB/d(OMi }-TZaF{ rϋ`!DL?%%iB^~!n lx0Igm%%~;(>s0xX?̚_.!rN6jNp Yϴz5r pUȎTMNaʊ;gb??#=5pîauw bk˞TFvE)w|Cìl?Q *'Ou^(%D'igjvbC%99 ~/ouj̝9mpxN6`*_?uL@$M'.hP ~B*&l ,zvF`^5`֪8cm!%܆Aa%SWO5oۥLo,SFnIЧH۲fx<;?%1x[$($c{"nR,%G6327c}fQIi/͠RE@HcFWv XƴKJh|)0lSykA$)c>1vAQwp9 &R@<u=a;ij3EcVd̮tTJ9Pr'`ƽk%ɸWrs8hOc - {ۨs9Hqډ?Rʈ6;9$G78$'p~}u[w+%P_٥~@ؼA +p;``b2=Vj1txU=%##ixj0 Yh観RtOFzH/'dͬ卟>@Y$e8/eEt\"ZZ:?>I;oڹ)$t3xFRrteEHehaZ}~s $D=[3$}ET-졦co|Dلћ㖋ϳ<EEw#v^^H5HR"X$E imkR' Na:]X$BJ@XZFIVk ^lJ w=],\|GQZV]ooj@9d"-//vs%/ʌ93<̜ba_gE{v,\p^7o@T9O @GJ0ZvIJj:f/.xr(Dg[S\͚dk;t=N钹ըx(K%E?4?3#S9~`7,X6# 9u3fCO7^/y)jT JJ8t.]4pͳvƉ% IDAT2D\zMpb +:r_Dhr(I$+aKn/_g0|XiET1MPD|ċY7@1~w!j Ayi Vg11,eS?nB447z " 3YaVf:%8x|BKM%:|Pq44`R,]Ge} x~x2,ÖP έY˧N'99^ق7bמC?y%x tpqۮ{uSLu"-/{T A{˕rmװ/Rvwu UCS '8=󲽣6lO̩u.<S_]q%%u\` I{jh]j~T/׬ܭzť B0`ItBѥ(t|≧^S;3- yzP}(a0Y]W`m;©{_y‚} zz}2$R[q]nRq{\(B3>y.Ȑ$$CUlJ #= xL-/?8j׮"xk= z,|߭-p)9HʠzҘ> YY}:Ϊ_VB?(5n*#?GʑG!u# *C̹'(,c\:i zeki̫r=_\|.h8oNonE+ތ4O3|K@HJKhnz[!,=^.G] Mĸ&*ZFx;qjczY'RG ,ȑ_( SA*g_7_{A4V $;r$EM}&V̻U 0[;zO SU+^bi~"f#\A; (90S-{"eGK%af[t$;3C^@R aOv걔6^wnDYu-%lU|⷏K#_'d(d{g/)^w5!s'iã̢BPv^}y% ]4?yLFtv!vc9y^z$RP\#'ʨ ǀ߱$bR 3J(,̣jV2{4NE`]|?z(d(ї72͒}RJ=E1u>X>HOϤl M Թ ns 7Exw6o`k/5cWqtƸ5f"Ιwﻟy;~-sfՒ,_qr{@EoSZR>msƫ/#'#Ɩ Nb?+}iLgZ<,V_UoicBCvYhOXA) 0"JBF~fR:36D"Tü;Nkmr4 S/mҿé [kUL"-t`ijO8S_`qZvJ7w\j`-FK%ʁZnoN-Yk bJS e'*GEdZL3 iB_vIB(dtb#L\K($XGVY7PJJ+tP硐 f ٵ/ZoڲRцêm0gr8ҫ0_[SPaEOX GB! 9z=pUϧ˩5p7qq<|O<搔JƵq}y/" (.zRv &-;oTYPONJ{S=}loGjRY`0lܶm;s!ZNuNu]15 r 2M#-# w2)@ H"$A A*dZ$BR9.O ZNv?6_=D zb.xoWü?Pw \  ~( ̚ΔV{49+T;f{i+/U3pp wnغvnlJ)nU| R _CyůL $7'W/.cy|ySܺJr}Yd?4pPL*A cO_[81`,n`OǫŜpboѫE";a{8{A("4Om;7Hǔ˷UAġ8V)16/ k<1]* 5Lf FpVss ߈![hZvmҮB"NPo`'0Q$l3 ?[Dm/,Ci2ު) >69@ &խ 1 $ ;JKs@&lǗkAuz߹^xn+lh),z4"Jqjw1!E-k9ɺ{"eyɦ- nx} W$'9Omx U5jΰ *#&b1[e }i+oʼne1BvJMS ynD0+ٳj M}wDMa_g9MolnJ 1E~igNe~o[=ofav+q S7w}b f6Y)E D_dY)M d!2$lg4?5rsڋy;'##-п O|g̽罤x3EFi%ɑP k zIJS2G<$K;e >sߧ2m]*> :XR\p0H~~= liU'7nAu ˜>Z.d^LϝЄS7Sb1ӰF#_`QmBt8w!/$. uB _hDzZnS1~B_9UK=/z go47[j0¿ҳ޲FѾƃ7.~B@Er%|~3;{[MXoD'ǿ\zD G*7!+n|8Or` ѧ^7m߆6g~KȞTF핫h޼%s:Ks~/~>Nmۃ+CgC]N0։;dL3τm6Ã=CYt^b"$3?}~ە$WWd! uNIƕA( n mn[~[fϞΜSR&PTCAv&iD`(z~;ԕ_ʂUST/D 7:>ǹdԵ !$!~HWU`$dW7KV/=wz5ơ;i#dH[wTհAκpQǰ%CW!F_Q,Zڀ~M8Ixn9aQf=eB:4xE’i&l'ٱF` wc 06z2h6]`|./3KsV[:Q#2=VYkr .m5ʊ4yhfm;ˁ6jp':͸r-mP+lKVj|a=wxUkP' Ύ3UL#M[^녠?Ho 9$<)C^4><K^P7e㦝9ΜӸ 5'ْEl۱?0౜.@̢2{zF#)pGռ}X@?z~ TVRYG?e't3 _}qnk96C^m2-#sX|.%dfK c^rΝyܗ~死oNꂄ?A!J"Od D p|Z+T$W6,}푟Փg>8w?A/`p$7Kwȋ":$'QXKQQ>)I!+M&hTgaǫŠ.78z6Tk&i/U4Gf*YI)lTO.Xc3P(!9%nO}yS;0ZRƮCCj0 =Ce^ "Gfdj.?ڡ"Ha\~ōL6r?\j-RJ?]k/+Vs.^_c7p6fuP  XNi n\,X5ŗO !?k-&3;#焤Hv)t΃!}aQ3"{ΆB04=]ѪKJVyg-7MBƀc(4h}/ r~菄AX{Eh[t ɝݬV|w61#A Q jhd26GqЌǴB|dSD}ҫ]( ?}'PRf dy̏/cREy ͊],776Σ#ك磬ϧWYwpMoES먜= aËE082'cFx(󯙱p]~{{dZڻxݟ,}k)i JWlt%J` }|.j@A]O76Աmǁ;ួGHKKo`!t.!ߛ*t,(f֣UĞhQ,Tq_]&t.S wG3:!q<,J ]O;vU` D>3B6`B%jXLN#!δhBX2OEx;F<G+pIKF0\t(cxTl ݑ8dmZKCĊT+}ȁX CPDhŞJꍍݼxV@DG:2dzqnz"50Y넶R9ߝ(:6zk\at9_XfMэ>fW_𚤤ą-_2w?}df5gY$J2cev:5ք0vӇP1=ڽY߸.ahpƲDD9x00$,1!S X =%/ucs&);V7HH :Ƴ'tۨV MJ#rЇi@'l_3L 3gc& :tUW^oxM(7sgؾ@aez9&73`.*E9y*ph%7-f͋B!@ݥ}C(HDZf:^J˖-s;68ʲ%sAR]˱@(-H$ k~ 75%\q<,:.]]tu_P<>.tw߃P}=B5b{{' G>LjJ9?S,[{=v3򒚚Jqq>eETV1iJ1ոS *@j2\2 Q8*q'wrK/b݂kHv%lzi#/=W\8PN `,}@0!)Iq9ܹVvVyy/ϱEL*#++s$5}c|G|\>pZ~2ovZ\&P;}1fnLC/COAʙ2[FBC9Ũ'H1 I {jn!6bNߖ?e,X7>'DX؈O]g%m| !'D)9qMזEDn+E@D;#FO0#2qH$w[``.`HyY΢$V>y 83Hy5`xı`=%cys@ RcE4Bo-,RqboxܪxzSLY19o[C~e)*hF~滮a0CfkxPC>ƴJ..]LUn|^: < \en:!nSuqxa/CCd4m]5sTMbמC<ȳniGJIA~%T3RIJ!cԙ66m  ȧ>^ͭ# S'*(!t"dcc^$*50KN iVv`Fw VlAD8[H޵7 .9Mirxxv0P"7fbʐkHiZ;^Ʈ$ęg,a2d#q)K7r; QQXF^V{8HqI)f|.MW6 IG,wT )Yyd?[T;:ᅿ; 7v`޶VE~U5+꙲6[@uNt!HsC'$CX&в7Any[knuܧݽdg2O5͵w^J$.܊ajeH"Lzɚ鼲~9ڻxxppgNe5]1w36) X7z֡&N sE2j+t+G3?YeNM3|fHJN3 :^YfU3=w|~Nnc>īrt=}x5EYi! ɏS)\BEY)4.7CC^AGd.f~-fZaOdzsxk%|Xڄ ě /Q]^2⳱ wxQl 8L<rZda#ԅ଼~<n{ݰJ{b;]lE`n &Uc& b|x C-1ːE FN4X@D<۔=$ 0H(nW?=ko MƗ8uSUsR(.62S&QQF'dcʹ u2IO婕24PO?+}E嬼^;w eUBJɐw$뼓_>~ud3T.$!oM?Ӌx\vn殙㩸@p% <ȳlxc;u5ԬǪx7YEyNIXKpy2sjx)aW]٦3ԧ_DFF:gδ1R7W^̺ Z[V b5vZ,ĐZ$ N4N wY~d&3ҙ` {v nS"af4yUńKl (SU$|A|iH8h)SaDx 1o֫#+Kna*""CG?Kp+5'aًTh#/\"A[Jx4*gnsf.ں}K^[>|_ǭzd6>˝W'tɳ@ 06}Z5ɁGhji{ݏC1 WâP5En_[OiǨ7YTgSOP'$< B~tawZ: 9-& $0U4>C>/a$%r Iq.X.Hkr+fs[ W h:qUeg~m<ċZbʉNiya3V!S>1̧7|’E&&{زN=wG $$%)aʩ,Y5H 3hg{#?{0 Rf+A'~/qܺrK/_k`(F^~u e̬J \.s&''z~G,l.aj\~OETmjw=7J+M%\4`+AdJ$!;:e;o€u ?ϳyz.^sm0xK?Iw_kon/C~=7~[:LWnx,hn䪉c/ϯg4E[G͒YL^/Ivx'txinRj2+Mn|Sfy/G=]jl,(&#)t K?{Usg&IHHދ"`/`u]u}]]]u]W]umkC "R:$@GB2}>9Ϲ=s~Gjjj.˘"(--d2DH2[WިGkn]8똪Edc׃< \A[D MUh/3_Z@>!icW&t9;kKǗ(ڗsvŞN^(zcws[5jy˚X¥fUOoѶHVX3a]*k3NbLYM>GmyX<{W(\-Bv` {Օpx o6Jȹc : .m u\AaagCnR/Yf#*gp&$BNZFN'o'ѥgK7]r>>yf%Jb&׾<g)e}?yΧc0 $}l8|6kn%lC-Wo&%RਗP+1nKjew$X |>~?Orםil Z%'QxIHK&AMVgwQ%2mFe%tۗ醲 &T&OuS{9U1 zvǫlͩ#:]buBoϿl`͆mĦfPw}VBY81{|cS>"Yibxw=r g2{SdA(.<o|=wL %9#I8N{hZjzhῊ%)&;KT1!af#S ;;Ƚ+]T@Riz9L+{Mr˨ *9jk߾ ;o҃D؁/žTyp-u3H٦maѶ-e.(Uel?4`ʲebح~Ѝ|PbʍfxlV:RӘY3g6q6 į*TjzSJ1+Dޞ/zM Z`Ro[lEX8zqB dVPߢ iRUۗp6l[Xp0N}?,lE?qj ޅյݦ?.t#ņAJ9'|,G6#^KKW;+p9g[>zlAr| gsyIsutee]nK}^-3tLGNpUIls~dѷ;̞#<"2Z+H^ǰk7FN#go~^R]7HDEENHHO!*2А`B / $7Jtq8Q-I Te%΂"'W!ZBJV<ٻ|;5F= "ZqcjЪ)=LJgW&ta!Ly>W+$`%ۖ@ *ߥG[VEuuR"9w%q *C bk]:ΤY.aʽO3x],Y!7ߡwR= sG2k{dd#?_UHJNHfB:sg~07!@y!'瑁Qq._]7R$O]t{!!:BBB-vimJ7>mՃ>kfgb}39$hRSYY]k~2F2Eť?p+7e>qƧgpba4c-Gu+~NxcY|f-JnKIϙ|Zű"o ߝ\N8Ax?x|*ݨR"00ʪ+Q2| ݐm L1Diq%W&QTPdvdع ;wYNU虻xg<ˀw3 =D'5&=5&K_qpSP]GjLz$!CƧgpcw9 8_]IAH/@AƃO!7鏼; v``yye}ك*_^ȬKYb!tޗBpT;7,f̖W|~]ՆHOBo0jbsYjuHא_PȻ~__z∋BeCT*ˡExteŽքh25:(½vvxɉ#ɩѴntdj#j#s9CZ|[HyU9`TgM7Ssϟ#*:VG#1ʹ{DEgt'rHg&+3А`=}IB ) y .r#{q T_ыg_tVi@:_6شq&L}Hi)_}jFNr'!_zpIH[6L~GsXt׌^o`,_EKbzpX_YvN7~XͲ܍,<^ I67;6ph}kI׏@%66{//gKٺ};vP 12ӧk/g:#`IN~~!I٪=ʹnydo o,Ϥ{I2JRd 4亨ߚ?oQ7Dm[8wm(%1dQƋ8zch{V:/J^wف?<8SgI3`̳)IΘ7Iu\xF _ϽeQ:eUi,s EPfkS_ lPcDQեX_E5FFt\ȅB.XDtX{@%4HN柪/^Y]FAVF< >N$ $}~,yg3xpd7|s .Xwz/J~eف9.V jA'qR'ApAF-u9bWu)rN*3rTFB9݉a]fg=3ɌI㮆uMÑvA `NMAAٓƦ^oeY?au:}'jM#FΊ>8ihuUd2Q]UMQq)Ǐ[W'y  聸 N]dYl&`焅jOIa,=Ğ-;GcG+] }nϝ?Gh\$~~1u!)s3hcN'jv;]'wT[?BϤ7d?HF$j l}fl,ͺHBbPB7HZﮰbJs6hcO/PSw 05tɾG}n+++'$8پWQde5ZHv𰫙Q-<羾mr>=MS]J0_ 5F fXR#7Hd( Q :'V]=\8л+0؁$W!uL+buL1Xܩw̴d?3x %7}{;w6wm93LlX ي3fK5W3+dOulfMY }NfUee"G؇Pc6S<,˘M&::n˶VZI7d,Q Fq6DU#3!)GWq.Qm%oNt̆G3{q!ZہznNpZ Ypf&w=/W%Uv e'"J6KxqTʳͧ/=dRT gN2ƍطs i8t#Uay^, ya|qE?}2p#$_ombc()+`60;OSeh 6n-7_SY82 fޏ+jGf8_ VL'Քk9\zH᭹m3nkuK1tc]w$">96"bB(/.94n 7!ePZzRel]uU?Sd#C1%uܼiT1 8FBV@nPikYE`JJ3tp/^3_Νxt ;BΑ-"TU`41HøQPXXjUGSM|Z&{<5K$SJ1;Do4 AڱVlߺحN^Pv/tZC(_R|xNcq\ϟ]'XYOxȹ'\/;7TQچJA*Wcu&j{Ӈ,wzԻ>} r;Ε'Lʚvtά,\z <_n)lמUNtLpXe+]ap`VJߕ5q۴! @EiTi=tIc>GoiN;_fkLu_v;##o[@Dde0)R5|q5wҵugN@2.\8G||~he( $X9)+"fH6nލd(k똕1"2 s%'u@Of3ed&_gaBݔdYB}h>jIzcGQVRU\{FDZGQa ۲2>] -:f_ W6BprwN!&- 0fud⊕{~e19IaQk;ql`-;pZC\\-ӒP$$ڷiX%IJiia^sPU*ccdwtr!ر0ǏK.<|U{vžNGw^TI %)_8sF,À`ƞyӳo#4^,c#`/n i0荘MfTj骛BbjmT}X0s3֥ 㮽䎙[72W<_¯NnM\z2QHDb,?#3t~s>&\;jmulߚ]0,Y_-ĩ<GDz:#GNлg'ƿ pA<ؽh^FHP`䲃xj\zf 9*<@T@4# &@o\g `]Zg.3\y{ FiIr]P2vrB|-<$6ߛGTB[!R^͢+f A9o(gXdiLf5>h:1bhBC#48ۧ>co|ͻ\9q=Q#m"Xhm`ز֣ӧu{tQԔa,wL.oZDy$,~b:f5j^Iihj ×mJu4*UzC2pͭ=س$^Gm< b;_r3TQ~{&{)*bމv8S΋?z٣Z%q70h@w4>MVK?1XYY?EKgfpϛ:L۶qF@[w ;4gl|zwG [s9](Qw"6 Usr418ݪ# I:⊬wl66VD#λh6K\mg缿z@ezݒd^vOcs9#lzS]۸2@ 9R v|¶WNCgKh'F]܎*raۼꄖ0TH'UjWf<{斾Sxt񸈠pvmZ1MZ7?t1݇`4e3GOZp ,lܼw?9'4qO=1vfUU5f;*uwp=w1|[$u0q()Duk "J] ,~) O8BΔPv0' Rziu`2ٻ$ϿzJtN%邴T@G@.ZW-H*Vlc՟,^m13.z oUĈ0kƸxNcl6B~*iT‚¸oT&Is0f,Xg""i՞̬dd%"<RMyP95i^8wNjߴkѶg_bXg7K.O>LJ::!A~{xˣt(~VbnByV-sDYQ*TJԯ]10٤0?{ym<2dʹՏsFÕQSśk!U5DWWʂxD IDAT|l6Djd¦j* gZ ˉ}xy?3U 6CB0~XSAdxeVi,sd_Ƽْ^=uph2 bM&IooߣzM>Jth}cm w?Yoؽ' jU;ylw?O"w 5UWw>4pͺb BiwO/3q;pCfN-r2mZƟ*cݵ0]jHo6\,$"$"$$T*UG)l k4_wߚ! #Bԫ,CH뻏bkg,a6EthE6'ӪUaaM />1j霝ņE)kߕ)h%;ݤFTB:9*cU_ItTF;o/}pc xg5 PQbn8Eyi5ؤp2'ҲM1 a9W̹bL@B#&dTBB q^$Ԙd3fjAjlKl"0|h.!,J-00vIo sqjڧʲ3 ˤD H=ޓr hӟGN#-=It(Kʉ k){\1@4-8%h 4:%/`9qw_XG{ux8]w5oEcB\ K^:PXTBtTC7̖{1M?zpptaz?r/.'g*26 MEoKJ|Ҩ*-_Ş%먩bǂ>G֠ #Dd3Ӷ(4jF#&FcęD֘!lrǧ;"bC۸/YDTT8e!qcӱC[0e[ 4}\y_}ykɿ Q\QLJD 7t)`}nbΆw3@3d, avݯWkʹu+5Y./$+ܫf]uh1]Ms^TNп2ŭMG5ȔI M6LjB7.euӸvdz뫿8]ϓ\?>#/բ'p"XFɎ4Agi3Mv{x~JND`_x-@*CNV}FHMB8r,m:b*Ij{ ߬Ic^w"2 &4B$Fg ,߰pb^zm)-dĶjAfnkXL{PkA@Hمbta!mf}eUe:K\8GᩳPUVJ"$&"fա m%"V! 1\ͯTZ ƚ8BNKYYh2׿?'((:ӪU2|:egwINe $&rד@hHP}U,/ߓyij_(.8T❌ axRp3gTU Z)\[I,ӟݖɈKgdyDGTb2xЦEesՅzTO [2ͼMwțbCڢ$Tuj7NPN󶬠D_B ـޠFC6Կ;ݺ_߮饶S ұCk6mcר@^g6anٌI6e $0?{6n/Cw]J;CLS_Pn*TiZW],jVMDBR1H2jG1O1 SVYll6SQSsy,xEtֆ#yH Bc9Zȱ=LxcՊ"@oyE¸!$p|6<+q5Coށc۲wFsNqjAN<{?EMe5j8RH݅N \8uk#ԪKٰnFSqIULfT.@$ MJq\d+Xn;׌Șoہ5Ts׷PET#㷸/տ_Gx`,DžH<5+Ux>v<Ηy%HZV8[֔A:̗3W1u'W?58v ɱIV\D% oq 2$5:l &#5DWx>{q-#zM$D0TVе'?.Yтcdĥ# ̄5E^{,dHlg΍,q%%-߷+zdV "Nun'bYa)h b]{`Y)^9$fg㞛Pס51qD7J)\?G]83'Ϝ}'ӥbO.XY~VMBt`+PBt4>$jIML;N m2ssYq3oWӫW'2priF7ُfA4o|pΟȵG״.Ԓ""FL0klv̌맑=&DiJ+rN|c_f@v^MA&`-67V\N'qMˍp[,dUk2.*[!<_԰rr4.!Lvm΁pz-O;p4WYE)HTV(,K FNQO7"= k0T"9!X&9  k1̨TӖyl7+7MLxDuK%*+|h lYu?Ps;׭o:jh6֎jBE-NymǷSPOtko7`Fѩu o^=teV".^ǝ ~bEyDD! )&ݎ15{PQYpg#U2$a4Yd 78! ,,6d r3hTK>IliSq>贴c=sԳ.LݷvLM*$!HN&cR23&M$Ú|,¾Yč#uY3u!LWLۄaFS'jt / A〔BRIԔWZ}VYRƑ8;?" HGRtkm C Wpfǟ̎>Lsϕ=@>;̹)sg%ǧw<-nU"C}W"[Fӧ ؽ[aݴ`R7o-+}׬10o|[NfCFZD&2%II16Ѯ]K@"4Q.`01nB=cV}%YG a01̵a(3nFjr \:n[VW59u错|G{>56 遷}[~(17˯@$J(m,%9VY-2Qz Daǩ 2%Qx/Z=ONvAG}OtvU;0%PR}e:59zXBAށw7B͛X[_x^=g:z'#lXsC:֥)%D*qII|xu./r",ذNW .,;7ёe}_76pz&+%2fYh6Q) `2P^]AL5lB#j7I8[Q{FV~ҽ˸Dq"ꁙ#GO-r2Vq8N"$}1{vaߑ2|J&_s='(8bXi.ƿڥSTe_eYzߤ~ ݸd=/?·F3lT*'uC/^š "=qqTVWW\Dnh_^MTdS&<>qOMۿV:Ȗ?ך='Wn!{tK7Px#pt.?64D]?ZEhCX~8% фTWV@YQh0VW[.kj k%65_-p6:Y$%?Oyy%6"@?:* %9ɺ ٸy.Lh9緢y[˾\y L}uCofi:pJ"%eSE8F̠_f_:L O% 0]|ʬo1Pa)-Zx:d$"(0ش-uv%Ff}6O4_@w=6,VKꠉK욕Svжmsږ͊3QL9эlj/Y%["ڭp8h! _@~/dpyHyڦp E@=h&}>p:aM=]S H::v=rP0>ߍf}б *Բ|:sv +>CaDu\IAEs}Ejt afJvK[t7tbv `A\(dӪ 5hE^ l;s%$G\`4nyB((v{Gzwxh&ZZgw/2s':E]t{Ζ;,vzG:* V%ތ1KczbILb7E],[x';s2bu% =SR|helL-(8re4$J DcHv.6&IH I6&Iɐ!4P=yWYFN=hd`0)8MNG?!.驪ZM\X8q@cXKJ0*%]YsNdr9E^3Hݴws`x9FКғ9oDSމoCRϧz;Z=I(Yf}oڷ>S6 Ɗ՛2[c6ĎYi?xz%ξWs&/ZʾSyl-+Ֆ+2/7-Pa[{9gďOPɺ=֦ ^6jh϶cGSҎ'yQmVǢndVh'L]4)~UDGg2bi7lms \hbQS_Kςfdj&W$!`yClH%Kpr>7 B@ kK5i0 N3I^ϵи8fNҢh[~k8`;5J\;o BԱ 9sz߳vsHGe[z暇<rqzxyݒo(85< IDATxptL&!,ަ?dm9(1* 3xA+SXc}}XV̝l]h Z[ő̠ &;R]llPX^ȁ;.&'&2"uPk1!G&w2%@e+xV8ykջjKsF8I{V22u1D&uW >Naf4q54=]̱?6e4d`KKhM(/»KfQ"ً{bJoO*CQ@|k >><Lf3.фBȏWV{^V¾}$ɔnS6?v?ɱcs9$15} o%Z뫻啙/^};>A`y׈ m˳3Gcn]%1atޡņG CsR_ }fOLx+5]0Zb5nbQ7Fʥ_x7=Du9$4x nݣ>i%)mhKwΎCrOp<\E&Bs 9:GpZ*cu ժK.A⽳ᧂдht3 cWx@FBl u 2 Z&e̵#aE\Sxhq$lӔʨ21,暥P@! %5:!K)N\Si]Jgj]Fj,fDwV-{E&€1[,l;/7/_gj3&ga OUu^yGh`PMs0fʛdO^^_&Jt5z5sy'=Cbס{YN5!FL暿IkI!#9v+|?e•錚G篭x~||ݻ~[O'bKwayЧ3~XŐgg3Зv@ٌdB&^)+@[\BXj{^}aiPhlzu ~%s}  :'bb H B|J Fm+ @:wÄwJ@%9ʲW>a܈~^OԿ9Ss6N3{)xx:fÿ^_п}?B}Cn5z=&0p:’".k&1,Q|-xs{d6ز1k޷/ T'Qz0<3ozC{WZMyAg3%>b×34u0׍jBRۅRk1$YA82EZwsq1x] % my\PhʫݣDp9#H&XY\^p?.n4 *j{j8]Xvྨ~*91(q*=1 ;`/c*ZUmb4m.=kh6@PN1uBcxp>jPQD{З_?`́ķ#i1,+")q ILo(~h( 䢼/5`շpmkdç 0_G$zddyoKxS[ƣb[Baxd6c g6K}E3q׍p$D) mg^k b|y@stT\>X tiɟdiA^P jC+CиX5%oXSE3B hbƪp2x۳%Khd]XOP&͞gMo@ M|~U|=m3=J<WB\&C,3I(*.eæ۰B93SQ[6lb[vmׇ&Ogun*Rm)(2L=s1*u #T$ |(>(=&1>k3pvdL&u>)\GL\!O.E%WUoϭ2 `2_<<@n+?0 Kµ#"::T( ޻xy @\K0 "ꄋ/~׹F9IoJb=clƅKΐ>[ ?a(4w HXUa l\ԥpGps9+ }(<pfb{*g):\fy5|sGQt{1!ҷu8ϰj,El$MF-ߥJ^#>Hf9QxB9˜><B$I=.Y8r[J%%E2@[+ť^ , ɞytq_:8׮xqh׊j3|5>vy7xOs8\Š=+ycDF6йZvg%ӟw[p ˫9TUVB =[K V4>Jbv'0 3+O00> GQqbZ%pi7Ģd5S<?~;NS,lӸ87̇ӏuAW5KvUV1}$=j/ =gǞsYN8}AmI9+_QRߘ'"05*,Σb(b"lF@CۭPlڼQ=Ah22[ZŒ.>}/"IۢQXa;#uGj5]Xl۱/OOrcDivId=+,=.vz'C.?C~i>A~2wV?0#⑟_1qL39Hzל,<ɝMf 㱩CTWZX?bʳ W!f<ڮKkWLOK eٝ.Qw`o3rR뇇6rzuhrԛGuAD,Vߗw84/g8Ц-}T Yhh ;=;p?$Yw1݁vK&y:ƽ]CkTjwñp`RpŚj*%VXQΊد1/ 5`Tp3dðfm 33?o|<%r_?wl[na vF] JW\ 1֮2oo&"v%<ɠmrRiTPA+l䊔5 VW_ٓh2v/ HwYm%:,FjAфTrFmU[0~RE9^:92XxlƌhEQ1+陞ʲ_:I oǪ?cZ3 /\-rd6U-=B}C}BچOq0o|]B_ ޻]#xu!FᡉOƺ}xw;{7v|39BU]#㞑ҺQ~I~6.9慭s+ 4(+8?<BtsI,5jFH/6c$$At!YkBb3 ~ԛnM;caN٣} N>:Wr n. Z*~ZmvdۛV!IJ`zNbtH;PHF O5H-Rā O{9KnXW̸c֥ n͟wg m\K,q S5::C~*@B&7`(\˲+;}~]Im]9Ur珬ܷwWXVe`ŤQcT2eܰfR;Л|uǨWr"ŕMzTra"Ġ#*=rʮ{7 5mAӃՅ5oJD,Ny-DS(Sm;~5#R;1GOTJ%AxռR~\y_5ԑI߂4^ ֟?ݦ+5~} Hl:>eE:NL>Y Te 0: /Yy&8n]Uy{rPJAs`3]WCάBGDF%X)Pb3hVd2Zf +G KGHƑlu-[RӹmG\E-7:Ƿ5U1*}fZ3g_)q<4t3>cn,q /*0w#*0O6|JEU%|C{&gG :ijÚ5r%:(=kɢ[>ZF7;y?0:yձe L]@ۜ(J֧[}ks !n@EakՄF@BSM=@#9 B&VCEʹZ콋D_s-KN@&s5miZZ괒2y;=헲2-~Ȳ6 ڷ{z"&!6!~x{ie F22rY~/*{LHDo0( 6tڮǦ3fznFEϪ{Uݒ7/*\fmJ6]Y|,6榡7 P#PYm9Dt^]wL[.њtv bBql> ]Ǒ]Wo+!mGp\RmLgPTQ?uy跏^Ie+:ǟтGH4F~񋉠utJOMO}0:\t"k֮PAt?,~3k^Y ,~ź;Y̓n BmRn;a7J+ƣDAg/E's ;`68(1iVG$űhd.%gR^TwJO 1at>*5쫢LRk ؼ<|*[Pjз:-t`2[0!w.~}SNѾcWL͈Ar=ߞhooy-֌`ۆ<4d*~~$<7%c Fpk _ľkse6Lo,H4> brt>")2QƘN9Òu#"o>toۍwK0*m$cFծ)Qu^'xk;s93{^Ӊ8'HO0wyx2ɃU 3X,hp1 +2fRמ/뻫s/\$Wo[]Ocڳr:QpͪK WZmH :Vp;tWx?> èF]q ј$HX(-˕Ө/d7կ EqQ{y\5"nkrxv6|=/ШfThuxD2OhZI -9}+6y z@H楿1} eqb~BDA*oAc&bÉHU Nhߗ;r&e|<d*}JO }3P} ڬp Mz=2}`g EQrZG\ (Wɑ0wg.^c킹^|Ŝ'p'Jd:^@@"#;0yE!S(;Qx~M;º(×q=g]"Uì IDAT~]c ZP"^GV)E<ÓHFĺg(gZYnm";⻿seKG$响lOŤɍKHLG{'s]??$D}Nt?sЮ  ظ(Eov7!,wqJkf]dG5UšH 6`,suڙX(0bDw q0bD_%t*併|' KeX!׽^*[X~ѝ(/k~<*=_~fĖ [:Ml=ȩጾWΨ_B_ġ,qKݡ֨GUOYR&DG3/%?痽̱c 0ʥ@;U,[MQ6gH FjJR.)Қء{ VSViڢ3}-Kn~^&yhZ;, :=JB, =iӽ#%߮]˰&;&4,z_^}3ҕ9t<Z@M+P䓓,0h*Rvyc-I@i~Eݴ _/;X9iRJ9qT>JO d2?|3XJ & E [PQ{ʔrJ^}Wktk,Eg p U% LD+4QQx89Sz$z w ( LÇ3Y|GePUxIwGzwz5@$sXg*2oǠ\=*$IX7N>IyU^J/1v?0\.1[?YȀ]D_=axiƋG'k/*Oڄ|\Y+G7\65__s(}k%-&խ5KfL*YTC}C{ݓC1unm? +Ц@/Kvw.v_jFt({a_wsٜE bFUYٸ۴Eh {m;ppOY{㉸.Y x9ƄZ*.ž's8^s~U3&@pŽp]VXuu)Z41`MT* oF\|$7"Z=?M~e>rw_]ΗKWά^W0:m]w<ӳJI]0I_˳_ R+TL6U;Vs0ۗ"}>>^<זUjїжٝ`OR;t@SaF#kCYQqA29сQDE-*@tłQ^sC5;i7zϢ\h'Y@т`SUZΉuˬ>'ibeLF[AQyj0M:dN^Knm?2?=GBx<2 1dV&* R=5mR+@_Q \ @JyGQRTQLNמ,!+UdC&ʪed3ޞ*s޹5^7wx(~*5X*riPxhJT>x ;9*3jYr<|뀕cM~3G]Kb"~xoR;'3j l5ec`^&\{#w; b:gū9 W|eȕ fPGPlo8+'s)8r1NT"݉L/>SGcQ(њ%U|);w}-\kϮG 4)?`ډKK ^Sr"ֶU%e$&$U8oL:=r c!0$nHYd@.Q+g/aSZ}v|;yyhRcRtj.^[]akXXk9p+'+ЪU2]Ywh=}c@thRd㑿V{DLP UDFYQ\}aOSI؟}/~#%#O\}z;͵물 SHu dWeow|ǞcHLCga4x*=R{G?AyHw0z7i]cNUf1\Ø~Cu 9<6oj=qD-9B (\D}lR{h^5-6ZHu2uWqGTtnځ Mp57DصIMF .,ȵ Op(s>[ak[{֯\fz{=]_ k䢿Fͽ!-5iS⣏sהT+y,%ZaJI½?Nku/}QPN'üwaDp4JՁb浵Zq꼗xGy >-&TWRvn߅B@&Tju U( A[%$q]^_O+8{ ڰGGEeRerz,ZulGXZ6*% .@}񋉴C8z#]SV2Ygt86ΧQ`]`_:3Ľyk^& 9 ټg/޾hL-\ewlPSfO15;NQcv=JrmYL'cn5eEBWVmY%%9 xItheyH% PjʉtK@ 6JuPD2Yl_`t8&&I|"Z9|>3M뀪D=Y3k7ցdrMVF y$2]{Oc]WAP!㻌#774TRn(C&JZL*QQk\z.EbcyGI+Im2AWb:v^3t^I SL?;wTV[ͧ>LM}ĦOށ0: zϴF=+M_p$(CHLj'VCETCdbC>tBLX H݊wGd@$:rʴeіPP~SPV@QEU*\f>5>|{Z#!ah|ex"{OFllEtZE Bs/l)O%L :ky MDjWzG*{Askɂ-hjs7#B&յǪ^Ipklι4Jp,l]\J.]-eFڵTD~z[(ચ8Qv 5*\~^=L NS;/Q]ѣ{ɼOlSĄ\q)x;_&:$0۵ԑwfni#]00'm˘sZm/c;@& 7 BDfɒusۚdbA&``4R(=0KFDAĨz1L5y.2qmxח񋋦I>6 Nf۷zΎ}&.)Ļ$JnjQ(*۷;gA r.Eܚ/ߞzwś#{SXAkFˏLi#Bd FQx26Pt"k(tc՛_IeH0(B<;`Äa5O F3yD lͮ/>+᠈bY6juj_ˣij{}LlcPEظ @t.tЛ!i$UE0նȩmd6}&]'p4? O'k:ԁ x䧇)+ʤA zUzp[xחk%G&q7h(*)/b.Q]KV~3{@Ǿ\0_ľzr}صFڅ%ЪK Xk jzLbc䷭+ #4;%!5ɬ\J r)7$IX$ ńlBYENKyr čwTv>Q!<ȵy_ OjZ?s4^atsO$\s"t2t MBCgT  nM> HP/ws1 qPz5eg ϡP_B<A]ݻ.nk=UBp&\+8/nT%VTvQ Ͽp3;{FɺMrΧoTo_PV537DуH06Py֛ȁjbʾGf<eKҒj~[۔{X:} ?/z*^*N6V{5 3˃_}6B˒)DF[כLr+;ݴnS!D, Q&*!'@r`BWVJ*˵Q^  9<}(y"SX%^!A?:oWh0;_LS&'9ڢw~~iSm<;v&"2U[! l?Qi#jM}BZ w>o+yu-_KYwh=c; dkE6kA]kTJ%=Iwq 2QiW**0&U3fܝL1W^\7NxN:&uؠPSz!ѕh0K·цMx=3r`qv=GB@!G˜$XAG\DS%5-P^p>j}5Vv<R NMp/wcqKS՚us qWedzwpPNwB.G^ l ~m o񥗖VjMcRǞz7:{9bBR@ײl/$g(eڼ>^j& `!Oq︻*ZjRybtmV;NWFsJYHŮLzL;殕o(O|1F7˚h7?E/&ݙR4~6ך T=LH7*%'2o $ :l4Ҧ{ fNOͫK=gu铚܋Fc:ŎC NFChF%l4 'vVmʰT)(6߭i$N'pve7{~݀ =Qxz j4PPѪs2J/+ORJ_l$%'N@7v@(UbũPφK&3Ҭ\BkkUa<]_tGrE~!bt1J w~ d0FXң{3 QEͲ[dAr$]ިGB|Bޞ/Ki-,8 O/  EGTĸA?PctH};OSO yʏvt;W G6ݐ_l CGa7OPS՛,bw7 u=Ė.U*dMy/˴xyh\Sq%y}aڴ!J;{9GL-PG `-u |[~bAD \hr\}0GQ3`ץGlt{͘ol͗dGyхVҸkpNbL<'qѨULz6)o1]iYe/15])W:Mg28~8r80ŕՇX(CPDe,W UjK+)(]X;zƧ3*& Fwů~L꺲`^l=Ф7Q2kBa$IBk"eP@^i>9`Kzx7 K]L$~x2m!}X`U03Y$>1|7F\/QM$PyjΛp($ pA&#:`D$P>|odк8u/_@p*;G_0_+cGŶ_N<{:, "2(,`a T#0Jp.:}Mz  ,vWHYw|`(b1#JQz۽P[u<4^!Cڏ9frtzJ޶\EO,:(1}!"l;SyctMsl.bFkRPV@rR\QR=22u2B,&^MB} vGg毙>Uv9DA,x(㺌_`tHr%w'|=z ~soOW OX3/9}f/pػj㝕2]n B~s6*W(Gc0i.lyRh."YZ4Gp-Wު9aNeμG'H [ު?G/\K6f" ip%R[PXQdbeosHe _QmSr IDATu4̭c}Q1Vy 0C5m\\GNBSq;qx khήPk--୷eOY Y]gO *#F_C[:+pMNS%yC<z* iCZt*!+P/w3{_nS׎̝V%%;>pf_x97ֵm~Iiu:b{_kcV_TNiw WlEH2q8>I{h謞f,!fv=tL\)EYJI;8^Cjߵ,` tqP`# Ylwm##m±ؿUZ|)XLf$ J̔O'zUc%[_YPP d 5>hx;*_{9齓BhIiҫ= QT vE+t{ !$9!9ܿ|ٝݹ?VD CJZ,H*j֓o~MIXwV]CwBε򸷈B=W^/,9t}*Jae雓ғty!Z'MyEEQDoSl(@WHNKbc z+9)FA%WvI{DOeX,83+[3Ƒ6@Vu0nԩ![U=yJCk^*T*ȓ ؤhK5xoBږ-`B5Dn'wl(ڮX~/S4EujH]AUe㠒|x7`&yKۄQVZt&5X>m?k_rm/1qtuԵfǟMd׼)^;KO9F uZ_޵kVYͷ}O^!R9GrY4R3ƥ ]\"#Z=هz?%EةoA-h1r@d6q1=JuFscVJ6 m&YEx9zPi_myLyٱ|.}^~KܖH0UB;mz&mcjC#{.C^5 ١BŜy};Ͼ,9m&;?o774fc2w$cǘֻ?ɌT.ãKKN,To0R^FVQƷe#DvnXRBT E& %R)fm(w95l3dWnXTrf._%r/Sz5@" CJU{< oZ-Ҏc+\Ǭ7{ jWgrv #kg#<띓2i(}њ+;-f)q92Wsqi_?lD"I{#v eV29JLT"d6aH%Ry룵煺B8y|;2 3I-v=:&^ho3白c3~- R?{ޥoc$]H;1( ^J|Kg^Og>ahmBZS/@Aچ-&v_˯{rQz47(3er4g? K= 9ܰShl>_39tO=msrAPJTsiBH)IBdWG6,mYdi+ΣXbF)WbY㄃ԓ\NaDaXD+~}&7II%8lJ1 Ȥ2Bq<$'0`&6H7 Mb "4pY"b_o {@Iv;> w L+ 6Ql=+ܑr׼sO0+W lÔKV b~ U*+.T f PgsfT1X9l 5SUUV[SUJ ٴͩ_JcEQd˛{=Tf/Z&|\|8r-JAI½s"# CmɁSiXj}DB DE9~8S~ʸNUqNR%\\zfLnQFłRSFǏ,V1Ze VX\}=&Jj4 mja;F wچIμJOT""( ck F*FTS9~Bבh<ZdEV 1װ=D3s P>[@uJ{;Nl=yK-V}kFDXa6lzg?pfZzŷcco?/Ocby>%:vJANFM:jܕv&_Nk ܺDGlh"P lY+Df7CQ1JNJ,|2 yմ1=SGY$(RџRŦfld0aAMJ!_7LzY.eQD 3UGW4~M{RͰƃP"$5)DaDcPU!Ef#.rSb,a4wp=/ԜTRrR{I\"A퀯:{O\X;Rykiوl`2b-(dJ6€P2ϝUr8[ `TH MmutoR5 hG'PITד xC f%wQ91kٗ\ͱR/"1"/_k3L>0KE AD{>dH؞oh߿ P&Ias( fM.3F-<H)[rSVۊ$5˭@TCjOj B F_jVjt:U/{QIB @\yFVD%F9Wwyy61jd7" }.p̬]O|rN7M7YGGw?[`4oԜg Uhޤg̫^OW[|\Eoٯc]GgҳR@/- gLh]JH_Ğ}LUΏ AD`u {*kQ"dSHҒ Л 4kL˿ux+d1q|?ven"T?DJ]4?,M0^]u?AGKUYTɋp/V+.5G2^ Bĕ,rN_`Ȥ{k/2rIYo˓F ehn:Ltut+fCo$ez>>~XT[JLE\YcB-VX'hV,^*"6_Jиۆ78cq Qh4JhE14"8zy T!GH RзwH'aגy,j{ pWo;y iȄIgsuMz,V 2 H)r\Efa:Gbcz)5 jɴůob沵Vk,,ZpS#hn(R+`L:HhMKax.㙻eR.Q˕bBS&=wF_J P%~'20~߷-ҢWTo4.9sj-*{4+W^|x^OyNG_L*s/U7Tr3Meo!ݼ[No參K"}fڠWmbBՙW~/P)X-uR&VD&# OLnJ`zzql#LWP<w_C2IOZjR|N ٲ{$9uκϢ~Uu{L(1ժʼ}׏jy~,72%VAj48B+v}<]a {ڶ?6G(KfRWDbErj ;5(r}^vIdP _N}qȤl `ngtI$ug7LiF)AR=5TLqv.v s |ZDdh}Gi67^;rQT[ONƝČG^Cm&e>_p aBQ%8 ]pQQjIH2TvػcA@(d Tr%JTQ {t4TՖ9n? ]Wڋ$g_&3=i0LH*4~Q@Ɔ[:uBz"lGVqtkZd5зsW\JfNOu}AmLI|x\rz6ed假ŀ:kn7-⧝`7>6 t%#qR;bYÄ.dgąѷEjQ9vWZT Z !ů^%5*E*Sk-22Md搓hDo4 -5^eϲCyhGr@rV2hj½ݬNTҼQZ+姑^θCu$lMJ593兯JYZ£&2}k<)nG\GҽMp-௺nϬO`^i^?ݳPR%Ҷ3j)>~M,u6vÕSTu@Q/YQ% ~olIv@=YWjyX;pg v!Y ^C;.$>t9Wmy@3o4R@gӳiܝYz~!('6bX! 3tj~X]*ڠL]4 'qaQșkgiSrr5J ~E|×60 ?8?5'/7'sˌ%:.;')+DV銝*{ ł` #kwa edc$iz{<N/TAZ!3kO;ܭDǑo3FEzL1cc8~-ɉz3mrǍ½Οl~qspcɁ?*SocŔtR;ҡq{hsXwr+R%5ߡMH:}e|:E!STRՌt\ʵҐh/7"KtF|h<"|nMRl(fUy|-'RNRb()SSj(6Dg0^@^q>m1e$c? Jx|S\ʼo7J=zSMOпeJ"ہIMG!Ub-ȥr&v.p'o@WŬ*GɣɼjcVъl@&Uɪ|k~ݵfxpT2N] r@w0G~rDCa;>Kϙ/V8 5 ĵ#'i6_0L?~ku6C}a!y)[LfJhƵ v`WfnX9z{fhOTiHe-Vl?#3iHToaq12a ѭ0Y3HNA?NkIrܻeE%:~[O.'؏q./ʡ՛Y9K:xQH@J`0?шg1j Զ\ڶf=;<ˆ7 \l]X RäBPz4#Ʈ1@D#GXvj M( w Gz]9Uܴ4u, j:6K YwTlĔ/ajB+dvv;RdTڀi8(J=޵1 /7~n27%/gP4mf>~ۿQo箟g>TR;X)2q^>eWԥ뭿V#"ʶB fujUbWB}h {ʩ/mVDIJ{ݶy 'F o٥ޟEz]>‹sq:!*qfxx9yֺLG/{[ewՁ&OedcnR:d(*Ft •Q}5^ulC󱃸~y);sHº'࢖QHsaf.7IiNb2ao=T&xmvg}EgŜl\>cӋ~vޤQyR_|fC'ѿc{F쁯rl>ZtU{aZk$H[PCi2Q3.=,ټ{fHRcOt֊C#˥ecD"HWӰy=;ʉ*O] 0"#Pb4Q[ގc?/W7mCc]!ÐǑVyah^(pD"NNClRo4%N4蕣tTrn>>{k- ,gl7ow>jY 7o,>@{!r/]!+iуuj ,(W-AHDFǁe o7o;J"?uzK ma(B͸N/*Ơ^ӿfVlـjA-Ĩ+{3l^I]:_o|:zkױX6;6a4?Z-xc~XE+2Y{|[KAI=>"6eMN_=}tHlPKڅcgʊ K,ݸBE0%~ŷFmIeּk}^<\jQ-sI`_+) i3e/)Tt$[AOj54{Υ,fZ1UکW t&=H  #i8zbC 'RNXr]\xX^A`D\ cfa&6ƼW-Xgl>MK qnh1ug'8RLAl&@-xUX7[>Ť7Тo:oc[nO-g @tVM_}+3z}'OYL3Wym\μisjR^>DوJ ϗ5oF FNlBud ̥dVI?sdDǖ1ة6̉`es9_>6-ѶY HwB*Ggԕy]9)1phyHZAL2HR݅6" Cn=$DĕA ygپVъҗ=\֨vmtj3{UsDQgKUoM72&ati!H:H+xݴ ,ڿogB>3gcl:yiў>O\~)IIaׅ=쾰 "IH`T7,V+B5Mf3Og]X,f\]iP:ע7j~}OSRdx ςݿWt'Y&vPM?ߗ8ke<*O\PRDCh`rpvt$$)kOZ%H/7ûəcbέތWt8Фc v,X"DRA=l4L%l [Dfh$)䊢A[BAH?qsֽDH=pp-nD8Ao<\t+Ŋ):"oZ[}P\,|w|ʁSyu(rM!+id)g=@H@+Lr._p-3FhIOث\̧/ɪ/~gq C#ܢwM?vc-R!%Ț;i9 ܬu;i=Tj8~3+6a'WDo 66c@k$,ڱ׆OOH|e6O?rO]Y_n٩9{J,V`FwԅGqR%½eRI QTQlђ^j"}(A^Q.2/J̅\-E_T֑Ie<s#>~RHrOtmS+lV\gyҋY*gO[ϡ3ΰ7q?%BBۢOY^f1ravJ jƶe` 랅m?(`Av_Fx㴏H(]3[̜H=[8t7{W:4nK}_ #Q"m{ZEα>Lf3 mŐ٧^uՉ[ /a\NF*qah!zɣ]~d:'RO2gFYmÕo|C al=]*& [kn O#PD"uɃyc -bИ~8e8^ 9#;)t'dѵNzֳod7B 5jjǪai*z_~u)[6C^=*TnqP.ZTX/պ?"4WŋWj\.SʾWl>N;2i]@J,\L6;sq Xk$e$uW. s3}H%Nf8݅uףVh5Mv.?#<{l?dJɬ?T݆)')c׌#PN$=; O29:AjT&]R ׮b(ØQ r=TɔfOPQl r }?EJYI.;U\#hЪ"hCT  >- T’׿ y8q# Iwsןd_0fټ:?[oT c\lXǺ_ЈyM-9W}߼6K/^ACڦ5ӿ٫f\ܤ܈]+r2Tih⑳hs(1`-8kkg{GT*s_T@OKh9^ىޚΏ81psp%op_£GƱ+yy+:p Vbl1tSTGNd칰+9){cޣy@W. SNZ$O=v㠲Cb*(1 b^Hgέdd!q_x=Y,| đ#T3d lO6~G/'O\]ӗ==`2w(XH=b&O¼n}ݓvWd6WW|.50Ds?'Qos)3[Eq<4r/Nڠ n pe9By [nzh*ֿ>꼮gȄJmTHꪼZ!(} 5-Bοκ7U+=Xv^ohk${U[7N'j#]| VVȽ+~:+'-?톱e RV2Wsܠw}}+jېL&U7USKI]W0͐2DDv杕R p'-NJ=bڅ@SM㋍_&Y$af0JQ +Y2ћ^|!ORqqt"1%#q-ZPjmkӟSԷg72,Ai3?MaTRjӳ"w ._)fX\\&gRܹWvgHX\ i=;XF]<6| $0&c`˜ fjaP/61&S}έ d;3u8o*C K`9fJNw^<ރ 95Y7؁]9lzG#(8Zt4!ByX~)ʻޮGj@W@~I>Wb炃ʡ/$\ QR* q9i-OQKmHk]؎WɨzfVoH7);>?yZFrL<=i;7.]zWF6H %fJsqy 7<"ڗmG~9I9pvF4oBN^1XX*`MzLfsc[0m-7FkBڑW½3}L4 5wO:U{MV2Az-`ۙzпe?&{Bd-OϪ#:TU-+[lR"~^w􋨷~{Kx7&ER*Q)8;~6Ȩdfr=\-~ܫXJ$H)f\6lPf g~ěæ&5l[_=y9oDZ&7 &{!)s7CLq<4j/Zx6_?Nh]w5PY;׶u]l\od]WO9ʃW6-V{UPU3H%4̒ 5Bux_1X5^:*,].-=R$Z @ ]­KscթA^oLo8^=؂_%&i*5\. *Zxj3N[ KEXD+u|UGVvf0MAZ 0[( il.DjN@gtxk%Vx=yWw+P"56-\iD:ˤ]䤦ͥo0̤TvnOůr ͎|9;յ]on;!YSmU/M\h<>Jw,cAch{\W>t IGvZK/@-yƾOWXǧ䤲:^:LNQ.NG IO'(QOO8ݛt+]Y܉c"V,|ލla$ϋh8teլ?zX?V*E",V [L齜<5M.z hehU qv' 1=HH:GubʀɄyU9?l)dq(ϒZrFPvAr< lmB e4-? Tʎ=pY{ge`X$J22 WW!XE K__YfQ1[hEdfN6^Ψ`6`XQ+:퍗'fL)ggElXOos Pvq--+z)P{׺MC!y,Lgrzr~'.pdV<Cޫv~gGMtyˬL_} LƈԵ37og/˧_˾L?D#`<#d#k`7A/eB¿I|K&}cYڻ"$|eu|\l|֟Q*YWY^wkݍt& " ADll;vNL6Xo_󹮋.Έzc"R5 *%[,Ɍ-p\u. =ȳV1zXMO[| -PkԜ:l<X$_[_MqtH]ġC7ڷ];1?Ez%cݸg.* VýD|+m=1s;C*I>07ƫ+j#KH X}*R)(qQ:V!'ѭG xaN [lW'G^m<g!#?U  R"yBʍ8y؄8;[kK3sF"Ԥ\._]m6o>wK+4Z GϟOΜ;} Jɮ{X1֥AhVnbei <*FhVr; iQ-^Ŏ/0n_%o/;R"pQ}ΣL;OzVɯY~//@"'ԙw }Me/Cu?yjHPՠ߅*ե)[u1<}9r 6xqԅZеdd149|ML Ls\֣074Gp~NDr߸p tjM&ĮѿC+NF[&H.8-[o} rd.>C [o#bpju;ŹE#/^'=vk@/Ă@az kWj@ז!{s}NnFQCȍ KK\}^oO@VQ#KdߝYs##^6cRٓg {}^.Ώ3d Cuvl=t70t`&u~=vדf-< bZjKqyA0RR*IdK],?o ʼ,3oH`ԚbFw|^%/йhޫr: mn] Y.1tv3rC:t~4vkT&4hܵnܻ݌x_#LԶ2Vǃ?dƺYknTh޴wfwyrQ-9sǸpniؚo|CCj#|NF"?b}1 ڿ|}޽?j5JE{iTy\ڪ& uDD̜A9{NpE"Ʒ'ו!$),eL{g6us_.ɱr|q˨Xzeo<|Qgޯc6܏V KџWRj; Yg8}K œr;G͏.Cujܛy! a[ 4KeUїg7aad݌z,$5c0ܰ4Iqy''O.СA^܏#/Fm Fu뚭=$zX9k+X$f`P?z`õ-,:;~ dFR͉G+ok +$r02G"P(3|=^1$b.8BicRz9gmQ^7v 0'пC;m|[h]1 v }Bz󿭟6f|iYЌlevKԔ cFΓ7WnKPFnUm y2S:M":%mw+|(}QF%s;BjNeżwz!b)Y|ȲC˙~bUW<;9[S[y4cP|>SlLmJK؉kDK3s<\\IIE,#`bd9-<L[&f6u%2C6݄T"s^jf~8vCަc}ۚ2P97g2ӤJUՅFƘaN&*\+p5jyeW#>':%4QA8}IW5BUg7IzHRͩ<%<6GOMvꮿںUG|Y`k{ ]L&, Wכ9p]h܏|}ZVX)U쾼+YkV4hV4YD,A,^Ϥ$<135Z4ZM#*%ZKKԶBD!UF׹T`I,_.^`4uSf׷|=z=i<؋zNǜʅoGS'OTΠfl@)AחH>}[eݮwH)yҘi)ՁL"cxx.hcѕ=~{g/ٹ5:XZu1ŅEH͇^O~jFI8]\[PX@!W0o@7ҿ\5  eN.׮9Ff-R =X9C&~8WGUH2Nٸ &r,vsQb-2{ qRrR=^j1J#Z[Xqt%/x/v$Gq띖 PHNA"bف6V" ec0ޛ7z+]r18£o hYF|a|s)*V=2T,e9VXˈeJCv(5_{y7k'v^4*:Ɔv ?oZ7BQsbA2NF#&%W+r&`(3xKF஼"-'..K`z}Hbm1&ft:MKW#i~;k1+웚pƆH_Y)( r B΋G|ATr/I1=i ©qWd/dӳdCpyc%"h5| dqJE9y#sbTZd$ ,Ыq>/{/fM.(Z;[ kn'2JQ-T).@ImelEழm؆gߑtOLeu'y/~TpB ^-X*$] Xd_ȿgUWϧ&{Xa;>Lm! S< _ĬDJ[?6 IX* [c 4V.^MU(H$DkG{vq5*%..Z+ckruG.&@w^aIC23C3f{wּ!KPiT(UJr HI!6-XUxzޯ=O,3.3pX2q #=/GWr9ܬ]1!0;XЯi{4\ "&+.$'VC8XS % DJqbĂW}&`bEd]-, mH\h5"?%S Ӈ.P"71"H]!314(@}y9Wl#^2>ZҸoGίLq?QLWK33~3fGqu/S.& l$MnPngp/+y㍞W9֩9qqU#IKm_2߻eҙ2v̼2j;L@QQ.__؊M-ki{йԆѭG2namLY2ΖNi=푈%4@U@\znn%W|2Q3g|¾uq6,'Os& & >mEJI2H ͱ02/=j|v][K l֟׺^+HEKT G+d_f䗥"Gì?>/K\\*`hԊ$e"/TX/N k̓ee'o̽)cڌݵ3Y0ZEfӡy8#o__ok~s!ZgBx{ͨLb4k;Q=ƌ8W~*: LX1V_MC!rCuTAoѿrC! Sl nɰC|= Q6 9ޯg3v,t}RSg6EL">Uw1f~#S:O!m$7nŎ/i⍅RUmC^̚=uO[fYJdypYIpk B͉컉83&4EX;D _W3Kt.q owfpe6~!^.hJԓFy3:XєVQH}L"hO?bz':+BE&G#'~p͍f|tLs@˰0 jg zxgt֞ZCK.b G*__ߠ_[\rXԀV>M>ǖ[Yo!a^ Zal@X#cݯox'^1ݦXÌ4hBmMbRbvW^ݫht5/;Oҹ 050ԠDiQUEw[wu!5Z~NtY:t!"=Z $ QH@RV.EE8[X5W\K8wh׆4H#"1ؽȋH *&c#J.GPG!Ȥb-HJKƮ~yY4sV]jU~:XUR]NK' R;.]*sg*|JH_LH9gݸf{WUܳ]oף ~w?Q\aFE7f ^F\ 7J2*k\F35_j Q "tz 9uҞ{Y h_L\$b++ZX;(*._384hL+|xf3~{``5SJ"Xl^ܖZr9'?~"A3Dž؋,?Z$  j>SѧYr2kO[f{6JHf ok_>8/lKfKƱ.cׯ\] fx쀅 Gϔ%D hTفK @G#Xmt[:y$iWopjjDZ9Q[[`cͳ!MXs7^epPЬ)?}0I3- T9;lyc#'Dzd)K;U(*LQTkx/ydž4rk9J35 IDATq۽ߢSF CRsR%wy쾼7T"eLQt TkֈqlQ6(rz5AF8w< /fɁiЄQ'iRdNՁZ^V"QQ3wĤĐ_ N~mؖ&p1"#OH ft舣cju:fŝ,9iV5j.߽¹繕AF^ɹgmbMF~^YY4vcVUeE=/eѪ"+{ym݁]-]Y9a%2.pG&af`esw{sJ39[0h=DŽɞ/9pe [?/ThKKݜIHǽ,YTA$)okT'BdFZ rJPaAI0-KMͭ1ŁY}g @֍8'n~?dݵ3hfy KcKv\E=A)V/ܐO|Ļkgj lIIyy7n0ƃv fÙMlܣt0$9LY~9+-2y"_2x"AT&K,=39>z4Ԅ^' (z #KZ5bj%YY䦒H2Kc bPxy`ofX$FӱV~MϪa"΋׻J^Q>+ b {ɥ:JW(#OnظӒC7Mhֈey$\]qwlEG,ݓfƲR2=n?0hƷT)k>G2Ħőu4 A( cnjFFHLJJ-W+Ϝ"_ЈԌL쬬kVWH09r/}\~pA ^k]ߒYU+rJ57ohyXYUJ>+?ٓuE Oy9oz~\}_ +tꍷݻ7hI4.ݽƳHȎÊ'.´RL L(*VjzjuP?BW+cE4 }#+xuz#5 bj`ȑ/ᘣLt*-tRG86A{r-_Y"f z=n䧤aJQv.rcC یD,l^ܹs=ܤ/3bv ܑY?,ezւ `idQ_?1j5_?I {7m >#eEFXvh9-HM+wy,X"~ܞ`f݌ɬV~ kPgjd23L -7s{5hO~?f>7ޡǗFM\]4: 3C3m  "" xyamb#6&֕-Y|k¼B~:Ho,7"ԳG#3c4moS%.=5'j܃_'//SXq&"VƖX08xqT7Y\Qɚ[ jOIP<*Z ~q'_p+O80͘ up{lDς\_/>.Á+ܪ5ijZy;=5'glUޝXn%F KPddWѣ~RLǂj8{C7s 4mB7t256i1}{~>/Q;-}{B9._gz:;abovaH& 42/.,"=m0D+iV{r^ ^ 9yHJJuݓX${"Q=MVk]e@-#\O-iU˲3%9'Rjez$#Ox}'vW)S4<@>z2gdg֜XɨStĭ[\Ii"L[|s>Ϸ@cF$f'F2UqYlRsRIȺNtz="Ϙ֣h^p'Nۉ5LviZC Y|'lMmߐؚ7U? dyB39'gK'xwZ945.Vά;q6Z$s/!~zQ&hcG|J!n!$+n\zיִ)_]Savt6Ȃq߳z\]>")u~'!'!ZAøq4mД UחVDHjx.ts=:^q).qu>2V7ӋX0~M`nh^Y`n %ݗCN`SL†0쇑 o9 dؐƽ,ٷF ~ ,+w }g; '!mkZO}-IԵ˯VI?qIo|n=ܘTm}9{cq̾9+_ HT*q6dKRI)VXgMM%%7FuR-?#D( T %^2Z ߯ 9)\wHT&z3ٸGk FŪcyD".mW+r)Z6ʉĥa0a|dd=:)T Ry k>VkOC0"ɱTXhP2FD$Eb 3H_1 ( n9:t΃ oSS2A@.qfdLl=M}!R1qqD"ȢXY$OԞø6&FN$b,8yJ}pkjZbaj @($w:sNF!=G\R2Mֺ,D­qdf+i"Xsw7uaxpLjcU*r*ܣ%" :nihW|31gq.K_XR[S94t}$%b htZ]ƕ\DkHI!6=8p XH֪ qrO8CÞ܁I^s`' AղRV_Z[[$&;8|(se #);7}LDR$Ӻnd r dbddS* %z#qz'H?݂ii.؛!KuX1膵#nFJf*9Y''1S7rttlة Xwz=יִo y*Ͼ!}ʨ7['gs qx{U1Zױv o~n=ܘTm3xp>zq,sRB db+*%8xs ^SkѨ{'slŸK)"ASvvgi\ڸ^˼r@E8S߭Q$Ąblhu:9syqvb Kgwbq.r/k8sC  @] _}u۩wĸY1}{<:RmISr9nҫa:C a\ٺ;):ڑp2GbH^R* {u@,UK3q{l&F0Ȥ\~_?}ߏVeB>uR^(F#NC?ӫ/1z6#/b*goL݇VP$6-tr94jP呑I2t2a0ʅ!VeϕNMנ.o;'?9Zڵ4QZ+rυ>%ί-CC03xvQ0V[.lcÙt BZ^:o6Nnҋ7{N+%¼B{ֽO=٨; pVYɷcƂ 9v&υw^(UJ]\5rV[N@UB.CW}vx ۿĥ1g\rOg-ќ:o/P6Ŗ[oJ\T$EӑA|f<Ús)+Bs~swacj\"\ |? Uyy{L^ 'r!p}.t0/.̱mD%'-nh".Ŕ:u'xdzWUbՍ-Y9\'aS6f ;Wsuʪ'E˭ƺ+̛5{=͞[zTp820tuVN#0AF캼}b CsmNꍇ~W]Ă.fީH ".VθX93@㘽rYmnӀM6T |l+]_bֹ4 ؚ {v3kڗA]J'5#koV[ضc8y {Ǻ%r>nc h(d߹W̒K+8N Ed&F8r)<ڗdj$rʒLeHP d] ܋|{:;RH E%TJ\]̡6X~9g ~칲WV΄hװj !SSlL@3g\^g@JI\zDĐZ*Ͱ D" 9}&+LPHX[b錱DΦsbրmX$X[;JT:@lz/az:w{ ʹUoaofGeS~³9-<¦= n1Wƹv"x[BXzW FrJ݌SsS~6ۊ n_s`G&Wi4hwh>%υf? բ0;^uVV4u_^>0bAX#IbA)  @K_ʯW$h;XHbJ9rh/ 켈ORuD\.ogGk+w"(}Kv&n$0~yFH1"|݂V,`Mo;Jqpb_JQ'u/3h6/ MCj}z~w\O] Yh4' `ԁU? [.࿋"J'^,N~ k9k%\{ ̲-dD,SYqt%cc_(jL]2_7k7^lBu3b uy7ΖN\p2u?f+DiӴEvv<,e@DZRT\cیflz4: ?jWϵĥ/2*߻I5jΖNt 3'8È﹖p;hh&OKSaiaZ.hUWo:V~QAzUa-2i6__jr~;/fː$ĥF!ST+qr7ﮝ㿯Rl BJzw9yHNB!U`elJԜ4 S?[oe`@jNj Ș&71*&h?h,鍙}$JVlJuv ^m#Wn!5*IZjŪLLm,K@cnnʫÆPqVZ~ 'ϗ}{SR __XѭG2zxP IDATjj:9~JUZĢ8r'ROZ%T}}Fݞ1IԻ?S?7'1 tl[}AbvHdа!LoD2V j$ߦCp8LέĈZ_Ųn y"`l{o-Zije))g.@'0]]4Eus(rZs:t,;Apy8E!_%3? u]:QwIN.aΫ^滽 h>w)94h{Д}߭ii쾲`elIkVUb/7bاŧ^3j=znvf j>eW]Axz~o+&/dpC{yVZT^W>@njec#E$$j",('O %{N!1T(7Xy+OǙ_6$e'!뜼e։hݒ5NcvsE0ݸ*e~ ԙ.Aqr%>#6 p=:K/Nj,ZS-7 Kc+,,050XnB@*"13"|`|+Z-/D " sw#p1޸ X[ـM0T(HMaĤ潾&QXvhܛ20zMw+*($%?9QF_'d}8qO`V|c4WaYFkNkk3"SP(N%..#Z%x4,iזyD&ΦXWX`&1j Յ2|g$jB&,cׯB*g=8Brv2mh 3Puo # }qq' }ȥ2]B+UJRbQ a[ t"p,ec<bhRz~5YI񴲇ٷKt/p$NzzlqqU22N:A+pҨ8t0/X[Lנ.UR 4h'?ŏOUIƽyzFNym_!ǒq.ӈSn"^ʮ> aIߏkYBH*Ł'NҤ/v cdaab Saړil~MkdEӱ2RrSfWeT:NNW<:;H^[5/uRIK'9;dҹpr Pi {XEѵ7J J* 6{{U_ Q@"wNz/F!4ݝ=sfγ7N*-jgv$do^Fƍ@)WegNAkd(ʁw@\Ԥnz w~[d[ZȠn\4HnynQFVx+r_rY/d2DnkB:{&/و‡0CB:FǏd_V!8/v8Q~Q:g''4s-V奬>&.B]2+rj6ufu)Euc;tJrrko~ l=S3Juz7JVgaXDs:71ZƐ,X=Ae?[ĞZ=oSu]fA+ߗ^n}Nogҹ*¢]$:ߟ"Htç7+?.rC\DADJ9cҮuFIow^ c+'Mٟ=7g'k$BZlΖ%?]끴,->Y%Y5z^?M\o*wq,8yTa[ դn.I{2&~ z\ѤB,2Q3_/Bg1w5YQj il]rW(ZyVKul9_Ɖ|@*:R2l5^^89Q\\AZN6,3Xqoz^ˤ^Wϙ; Lս'bw5fm_/3]Fփ yWxn9 * pV;cM&JKHUN {jV+.ԅՓip/U+Z^d1Ւ1(h1*sٓb,rb_~H/szjmcb&GPS+mrhwGy1cG~y>J"}#j 4ya=Xr<{ҳ nd >IpO?}..Ks9ퟳX;襝_`Yvẗ́{'&GOc셌~e =؅fr*v*B8sU›dzk~zGkGxqC'42 jW''FJAC@!rJsu$[OokK;yz bq3.zo:%)7 CE22xnc{* ٝ]ɻٓߏm`sQ|t5f^g ŖSH+Lo5w4yU̥zPį&Cb*j1.alxM~Ci9v|{q 7!- +}W7~ECGY`kQcck/_p0C¡PBG ,n!Xxӛ ْEқ9YֽZz]v 96 Ip "sE1l9O~Ə~^usӋ2P+w;ٕڿ2]‘KjNY.#z溅M.oaЬ(:̇ya q#ؑ=<sBܵHɜ$hsm܍L_x"7 ZѡVxzꓤmZjB.]j/tЛXFy%; 5QjћZg?G3m(rzJd4wgm~e6awaQds+qUx]#潕sHЃ $#7~Awp?kGNn|c:#I6\y3nޓEmvۓvl(nch APU«yǸ{yhǠUiyyt`5.oWoJ-2QdI)W2oy՚/q5-!!_puqR2qh?%%x{xP1~H\\88W2MXYGLkV3{Y{x-I|_qԼOMs8gF7ſ?䎑#}#04v>NPa <(ç9@\ajˀEF}gR])EqӸFKcfZP\&ݟQʕ5=B79r^H;Ȓ xh8KXm%Fk5&S;dfDsp(0mXJ+=y>:(ZӪ=ڎB;rpaK;0/'\JWbgسĺ~G/獘8 rBk0҆(u&n (d ďfdH^8q#־]ckKrӸ}W 8; ®3љtFFmsωX]>Ysx] h켶1rN&vI"£'ʢߩޱ٧xه='4;$IO#tHO[[ϫwEΝ[}O9ӯ/<8>|Մ: #l6fvYe.RחwSj#"ZڲT$I륄fATG Oyd2n}v{}#"&=ZWQPQdBqpΒ[3{#;I}0USea[i;+,_`mKӝR\_*FD`?ާ?Uz=߬hCqY9ɾYL&C E68G$x|ң^=_3)Du.)e=HtdBqo[Ȑ /zۆϦkJ̅6~ټ `ED{ow D9eR/.$,0rCyE  JEDPd,!>5(2 @|0g)d2d3tj3֔kl:3Ot $t,,-gv +&ιVM{ާT\tQ`ps#( (O1(f` v; <`6 ~lOŬ+g}/ZB*$'Qm8qÛ4 a=s`, ׿SV5k| ~pNy'v#IFϋ3gM<DE11a| ~B<=7nKv!eF!.ɜSćğO.˙]|~5pZߩԅb|* "^.^rPOt~7%fh/d3BLǷsS|cw0%&LdX''=͌G`PD1V1?Ƞl;k{?>X 2__ZkF+T*[.d5㢮QP(`٦ys$ ҷ?~rNse+xrVX"Ăɏp;q?d 'Yg%^[C}K\fe[Y4Pj.F@ ՗.1HX{2}fn2])DFFW0⍓J˾t\˃R$쒄b؀X9|]}9uJ <c Y3mt22ٔw~ce˩Mawl #jeG@N~y'&38|*T%]8sIň/[S06i\TNqsrfUk##zmXG3$<"%Q[zt <:#;}ڣϿl>$))!1C<:aƒ)%D$`ߑy69󥂻;E*ΐܦ`!]i;I/`؋O iqfFaJ89k{|9xM;~ KdIen(I?[׾>YQXũSk]k$3QTTJ//DAdl1S|3TN?n~=f=NȐ!!EJ~ ]/))57olo]vҍEU 'ٗ}7o(( "EXmݯkVYPT3sNӵ *h aa`omD܎Aod1uj/wah:Yx%{x5%v[Hkup"@{6GST\';Ot/^{`!G8|sZRRW{(1`nqK~~܏n\{ %R4nˏWTGֱ7ݬ>3L|VPgV]бǙ΁erN=٨<'sN=c6#{f6k[8Ku%L?ْlEh*7^D58 7溅ƶ;]/ePvfPZ9NM$+ءD  S BQZ^FiE98'Na|'wW;ỽ?5!WNp΋֤rNle EPtgxH)Hd15XIi<7Vnl9l(h)*oZ=oIJ`sgK@|tDລw̻m:wΛw3EVkZ;L=z vrkv@ x"1ϾX6K#z 2/}EoÙ}Y}EìU@$[0[o#P(s4BC)?^IN~!=;v́Vl3/q&/MƬV {2}H U?}e8s>[AdYxqEM;g|cG?;W3du|=3dA=+s!!~O=AjtQl8AXI`:EˡL(Ğ8~>}y}+:+J,2bE=V֋">hAv/WbQ>u o'oEbjZ\&o|cϱ̛>Wq4*.GյN{vpRru9e<"2rs'\th\Yˎ?_WwxzEN{ԅ{fto~ksBg4F$'!A&3Sjtݸ.,>%R<<|Ͻ#z (4 ቷ_w*JC$ fq0_Yɜ:w{B,v+R$]s8_l(ZUjxb.,v>^de4@(5̍gāݬ=[ܬݬs+M®V]nox_~3b+mVɊx5D?Y²cv4hP{[l봆_ki j:S|uclf;gΜr&mҀ o|+uUXIy 9R042'$7{1˰.d$&ғBe8PT HH1G%w[Grs=<((`ż4zm>`2"I^%~_Z!9/I`F+gxEׇ((d vۇbp" `6ࢮ]H".4 egf|G zϘl&ҫ0p쫓{`}ܺv⃻jp ݽX̀ q޲ۗ};`>|su/Cz& 9i|V`|*8\fg7H9v4G=U_<@I KEB3j4eٔ_9aDEorĻQ9-ɻ=2QU›⏓[lPRU/nPȔvOO}([ڤ[ pMߩ>3 kg c5dJQn:OGTOs>} Qqu ]! "z<*umf f%9lJLFQ&wfXv,5ݗuXk~A`hvnYc_~4]l UΐgN; kqٳϔ:KӶ=e;Elv4r ~FW Nn>|a5=>qtÈsnF>XUz_[-AvR*[97wyO-ivf K?Xe];۝6|FL[[¼B)6`U9xbR RjmA捜GbI:vX*aA%SQl.ȖS?eo2:%8P{:4OM]#k0Tԛ[Tib[xXwd=ZfqoK7/z]ũ8ncծCvFti#.Q f+}{ ːrJu89--vyQ؁n~˛slyۭ%&jʦ$I#=s[dW}rfCt3F.!Ilr!]:E'p٭Keg.&=Ӧ_r UPH*\l8๓fΣfa(ӗ7tTCRz_f{]lH;3箏!>kMHqU ON. " 3=wQfcެb8tiG+8ih;cu{tSߢ˩0N|pW^Ċ=+tb6LVkkDr^2{M᳛wW=Q&Kkع&+fj ]șd:ׯkpQr.&< ʓ3OppWgg؎W]>qql?zNly)L7UegPƆNthvNXjE혬&Ԋ'ܛHJ~nMW!Sْ~h^Z2׌_=6C-1<^x$ /-in$'uo)חGULqӺ"Q]l_W{f22m(d k9OvФ& >(}Tm72J2Y}|UF7+!,8*z]DԬX/rש/}#'e/mXc93dݝγ 겅lEGʭ t(K)5IblY:+_^f4X0*)(n#I* _FTSaRkWK 9/ Vk jͥ*$smy6[qr+ ْfoM$+$fK3?pvKg+Q|Kи^ 0 ?;c=z)>]frpws>ώИ*­]`o9nCwȿ<mٝv;8Ȓ?T(6va+ bc1!Kp(n.,^eS jkh$$ <8~7M+OP$c eA*Y rL׆dGo64}f7C*) |YL&[&OR'93nQ( XrJ{9oU*`j)hN'eoѵjoO*% ^]$@ޭ֯pJKxyp*.ՕLnY.R݃ŒF_eX1 v>=E.-JqAoc_\7 <8|*Ua4F^l/(ݿw׺[G?))Å5Zz9Y{ ^+k<ЙhWՎoHOH+HgӉ|3M:͡S'8~&, J,~ߵ ZƩA`P@>;Vx唗ɻs#JMd{]p6t,Gz>^7"5z$,VsG(` \&'5Lts B1Tx4fC*eQ@!I=8V[I?7?LGL*h66޼N.jgbc()+^#zq0P+v{΁5 !^|rsi=Ω ̖fr:}k'[T'R.8yZj48^.^DFWVP.tLI47/%;FޮAl@ %Ez⢢X{dC5YM|sBBQ(Hl8qݯhq;T\\gCǤ22nc"/gr}9Z7XAx:{RelueX|QKudejNm-*4J dgY52=l̑D20M JMn E9FeƸ 28fkkכ)+"!]H^XZR "997s5s=^':m\IFu7Je;vqY;MZ\i zl|Bldo0#N`0.,V+qkJňep:^};8vi:ow Ӂ/=0{ILMk]"#}ދȉ+p:~IH`Ǒ}YQkZMz"+,b9~xе QLY+PUs&/hNگHH"I<␏fJTn|=A:}3(BJc xYmVfCw'u,q, yČjFgҵ|?ɑ2쒄fV./ -"nU %*g>#X/"K6,m>'CԐwgF\ZoBӅ{18%9" '$Ш4Tv6;/gv5&^y[+t @ۆeBotrrx09?cZl޸mtiw}.Rv]ws(,d(5iiis{VJt%T*NU<=e Xm<ԅ⪒ h4T/n mMxr%lj qӸ+2q׺S/Wfʪ*"}"zo6ŠB~ڿ1]Gs(0;_[Iݢ:q*= ]:5ۛ_n#!={MV~Ar46֓gt#$& ]drv^Adg]M[=Zb5#! m0<8[\&͗WgU}'p.ܵnq?{i{h&˽{rMߩBafIskD.AϏ*SC" "Ccݟk^owF^**utiv vFIφ^odͺv˼Ju7}n!m^͵zcglHu`! ynӄ{s:7mv]lyj;|h%YM>|IfUyޙv_ym2~H;sIPWDŷw.mnWk37!;!drVau9D.־&nn|ύ//N7MTUYu)#u .2 D,)dJ\nJ!S\(f5L ffC&`9qx[ۿA^nvA!*5hV:;4\+{er zxSRUҤ{ď39y]Bb*lP+XlruVnW@^Y.};avDAdbx6e Wlv^c9JռEAdX|}4EV~[_ mKMtrf:L:pm6;>XłUG^g`t ܮXRP>& "CG3$vkd-nȀp}`d,z6+9=nՕ$$ưo)>1ss8q6r)FG;0 j]> <79QO\_qdTɃ>3W?VuGe;sݤa we/nQnerrrSL%/\l}lÈ("pqӒ|<=ɯȿ2)!!֝^7ßV]KvJl$'k„!X8"%!6#I d2$Ig_ނGk]No@Z'*uaGN3)6v6 W9wiU)Y;c/H17nc)as:|v8#o}^TԨdޟ[ڥE}zWcC;FVѬcEWҨBq:7uGEP⃏7nZ7J5JҪJ  V^W+ vݏ@@phC֝Q]GϯѻKN`<9eAe[Je<d+5iGbc0$Uu]1ʲohJ?;LΡ#aKh#vKw1LDx (P_zҰQU=) H`JAj#(335GNYBs,3X!*QMN~ش{U/PaN'2$z(:]v r?]w~[ܑ;Xi+,F&=?koWoc(6`ڤ\;ć}f/3_ڑV#jQ$QpR9YŚ똘0ȣfi 㾮~T]-&T fBmv;ieI|\4l(njB.JzNn9srVc2ѻO'fC^6oct\+(Շ?ϊޔ}LLpIZ7WG>odo ϳZFOxxHO)OO}!?fˡ5\o#0N.?w < hZ:90I0ZWO ـlz 8+7 }l=ܲ\c>$`D>@uҍ˸K/gOn~ -Co '6r۬o]x|-9(C-*j\:49RouWMdTb,pp/)!>3B:J+Y:7xf)*Pa^5r%3O[puN<0>⃻{APKr*lvg*N@uj\٨ٗڳ+J"ABT!a`ӡdf=%;Wɐl9-eU{λD`{;xtESX[.&jW.]SzI0ڱ\wXK pGޝ-:tYkNeրYl$ OО$6݁ji36TZf=v~b؞K*͌ZE^m99"]]pV9caЖā*䧠+۬?BV.Iф#G޳a}sDr^ :YtBUxPT~d:(r̖(swv̼ٙC5ju mS&q˳SPRZsͶC5UgQ'>k˰6ȥXDxGpsjFKhU6듫ƕ[몌U{(hpsfO{,g{v! (/ĔI<~[r1XΘ)3ɖ$ʪ+ruDJB:+o؉ro@CUUD42 }wn8%9džY~TfK s,a]4c6)/ dXNIO1/ :P$LŬZpi],11$t8C<6:J8o@ۍ`8gW̙ŴqcwI ˳ot9´eah=Q\{o.'*~^*~ُ (ǧGLe;yhٷ.iNYcゾcD2/o.ȇ7}Ϲ*IzBnhIRy֟5_q !Nwj[%t9z ]`ڨ?;vRT}Ic -@ Y3{tÀFnΦåxO39m ߻=FaV!q(ԴWr k6E^TKSPonuh$\>aK/aC駼ڋ<@$G`(h #mkapHouG?_j:YcHFnP'wXDB$W׾CRl"K',n9!"F+{jc|>UAQTt:8=:[=OuFUA@">qۻ;xfs}p5As]\e'G{Q\{$,ѓd;x+O7YTI&ѽ?]a]c[~K^(F̠-,dQV-,|He]=w>c>%R.orxY?Ox?Ͻ?99#xG,I퐕쌆Y%-?=ɕT;9Z_bȩ|sN=E!Ԭ C :ܯ.txI}xKé3őiP*YX \7Ʀo,-!%> 2M/p»k7z FKE8lf2!s"=z@zwfiٺo7*vk4[*Ͻ!ȞpꙖ3-L!'#G7J|LB,"JҒf+jc1[w+BR\"1F 1󍀀(Q2(J +{n!ݖqUUŠ3Rb9 +AGG) ɟ/qwY3X1w{ 3s۳o sHrQv j'uqk/V,&$9sUd[Jvr6;N5dbtm]S+Ymt:C>lVB&EMæ#_p§̊:? jGu|zӰ4K4[m:<~Oغ6* q47qۺU%[x_>6z S}$~~i%/yv[^8^\q]UC#_#I)aқ3)9yqz)pM1|Z:\z#& ΀(H Q(@b\" =#/{+2]Oő6o6 YxUUP՞s7jP2cǂ?vslዪ d'tַ7iܜ4I'PUuDe7rW[αc()򚢪h"+5 " ;y)j卭on5sFbjο^J]Goa(.l6ڹnem (<ڷ;}t{ڐ#?PJtj G E4!ՖBES/o,kMK@n?"e4 AKsD7 _n5C1Crwa5[6*$&9Z_tBUg D,8]UUI&]RbQZ0lVx5'zkt:]U in^x}O};\d 8p߸j***~W^aF ΕekegՉ~~/Y7>\oKlakʝw8(!9|o--7JEC{9#^ŇIe<Csk~K:?Y46)_^2~mέM{-U~Ň ʀ"3?*VH`Dڅ*Ds:=~%u%|^wb?1Fs(V=Eʘ7wbdS-zpwO0.A;CQtKM筷]Oo*|X2~ -q B` z|xvi@'tnr GĶ0s1&3%BԤ7 v/wu& ;y=1.v>+vjG5370FYC  Y@#+1vNJI"x8Sߥ9$Z†/zG l(,9%3gֆ^KTuvކיO.O4{*?L5XI:td|2_r/l|o(tcAF"XR ^-l;'M˳6[m5[8\QLLFf2b +hϛU_s=%|#ٚLSgCAQ4aijhj;QᓢOVSXDdN.%':;47eh^\?.5 hpf"t4\|6T7OPaO>QD]9 bLj(d?: .yR,^%`Kԫ܄qW 1 ]l,ބn?ѕ8]8=ad V@ c?Lz3 G1\n7ǫڱh PQ|(Ombp&1v,XUaY$JLOK^16Nsȼ@׾ї[;]_՞<&O%얟M5$fG|N!SI+-+ Es_8N;d;/˗< 0+w&*;3hrwb1ǡ M%Ζ!vajAs'px#ol]͎XqCt6ƎYb#rx|h+DzN{Lڈb_ i{|,Z4 ^wNzcOUT*zCl.hRUƢA Nu J8X} oTITrǧS.GT.ݞVAg pnihkV.wנ(Q\U䴩8jGuXWUQ'`M▻E r eĴ8{'tvt**'8u-X랅w}{kT㝝RYѹT;jpw}܂| zRڃ!vL#gKdh1F$d1bI.}k W`t;rk;Z9VgA8)Hsm/'{Nԗ]tu; 0POӡ=/b({&e0i#jP:7/جqz]eokD͇rïoo[NΈQm?NgW7>Z:Y'3?/ Ò%Q_*[6v% GW렦(}n|1^>56oJBQeQ/ZRPP1ubt*AP[5ÇNe 碮zPUUNq dIDAT|^ .gӡMK\=u%ϹAADK"Z{ *:AN44k!35-Q0:uh?2GP\[w6_?ZZR-<}/b}kU+y魿OD|7ВtkD#*]-}n&-LΙ>/FCृRҷ]8]tm%K/fE/AFƤ?o=<[yNGܴQu3yg{|vhٹQZG-nZʚQpt55ŲײջcH[8϶ce I8[WkpzLP]M.gHoC9d^ 봃D{(Ap3ӟם>9ćş0#s}]A)o3v]-8Z9ZKrSG3B wr{=m f!+A=mP5PX.+]? F|-j-^ًb1.GQ>{ f\^WFH={PjKjQ|_3mTJL>ww;>gc<5-Gtva5[X9*f>dt{Hϳs10. Ȳ,HHUUrTd odWn:qXT%PKDn|鼑zq28ZNH< ?^\uڱ qj6rƴ=Q{t (9 "F]5G)g˾Ϊ+3EͲ1YCM -"j< V qe?:)xphjma=Ȳ_ )-׏$1v6ȘQĚbin _ 8N[(jw=4w7vpȰge85mFg#1'jko;ocDij>WUiшno7ɧG>@!(̂![Q*+KYqںHKN2Kv𢇹{d%U`d3䦌xFQo)cT:t]tMwwǃO̟4S0k8l3{78;N6[.'u**/~ُIo&6LCG# #2r`֛pz4B'ǬaټUaY&L$T|o ~Kܼ9a]_"RARPE>GZK 53?lz#=-8\O#摫nFh:ܺOtxggK(A>'0NfGjQe]8h~1t^K.R@;4 B|XLtvzNL[9A.cu娨l9u>!#>[ʊI˱DAT$JܷqԖP|o(,ɛĬ !30}w$!m$RcEAJ{5M%;=]^8jiwv]133SbmA5;8aNc(\@&T"F \j.gpk2^1?@v?nO_,1+wfoH:)&LkqtIzD=?2iE/yfZRmcY5ueX8.N%ٞ@UK3Gd 3gtV6~:<Ɛ9p)ߋ_W|RyхGA 3>fM3o@UUU 4*5-_0vh22lbb;M9pM jPktth 5)5GXR$,c/1xX6`ho#.ъa:Uk?ˢy|t ?K;₅,.X8hw}vL,9-^]~FΩڑm{jQ-qOeFr=\va((-B0f"=7nrw l"~d\z~tzϋDnsXXno7qذQg@Qk<4 .LNʯo$%æMl=[2-]gF0#~΅x=>R㓩ia Q;ߓͣ z]j (lI Izd_x|{v%}azAK=C&XM>En(Afʛ+XL8ҒzWގ]N-8\-r[;[m֑"":A1`U\j:?EUNk$\ y>\k V֑0j>EW.f.p Iaң() S |5̙v|Lvt'+WN^V2zH;w0^.bM3>ىIKPe&t$Ba9rhlc K䝬(_nKw]^z^q8|d^t&>VʪdۣtQd3L:" ںۈ5":$ K'˛[ѹ8ʽl+՝Z:[UUӮƜ)bˎ=r-ŵG7q&it]uvhmXY|>Z;}ܰJ)FV吿d;Kw7T^Q;Ò8%vd@GOUM dB?mh+b'3T$R6Gz>N1=# AQT)UhO)Yz@4)E`gNغĸx9'\ܐ+$J(7]<>/"*h?b1ty_4u4WsZXc ͤ͝XC˫cih`m<;`ARElMf^\jZj5PZUI1vЭCVvE|b, 6; 6;#20&UUiꤣ@ՎޠGvçzk 5t:PE|N UQ p,b!CiimK#} ᥵.ܓ͈ȸ0`S/d8#&xiz*V8m8*F&DE) lR&dFf`0PPdA)zlvh7Q`5YrvPPώCTWQZCf|F Ǭ39RS!y(cl<F[|ol)bˣvoteK[0̨$1+w&FɛkRTu0,x]Ԝl+Ɓ1^;aecq L}$Y)9n |Fq!|Uwd]A[-ֳ'+вUҚRϊ7QT/D=j"ΌT%NA1A0CR{AsMNﭕ:UTAT@·_00 Yo*?;tb.etA? e\3*m:g`T)zZ!Im_k@gTT$apW|Y%8GJУXU6oU8#EQk^8O@pDLG!<9 6 'eiHX8O\-ة*/$m,7`nL=UAtȪgшWޟVɢ ͞ή=defy94 3j3SEFa5[4bbD0kLܙKmkOb\F~Xǟ6];Ha$HE=cPvܢ_Evzd)~Pklf-E}nDD;) `,G2xy'X8n>L]ŬܙQ&<>RnWPYـjPѭOsEhvq<dfLjF 4hpAtPT٪ē7=ɟdžx{j@R7bn# l~ 8_QsjD}5~rx l|u~5֠u:ps<_NEUn':XV\,^wWNZӮ s\unho͝oRP,se;wP 5c?:r t'F 4hp^4u4񛏞+6r*QM?|L)~Bq1J˪eM\?㺨Kg ~ɦMl+ݎ`&ŔDIc>70WHHRU{o?9EB4Gѥ!Z1``(i<4 Vre8;˻vg7 !9WvrF" l)ފ=uӯeԕXSˁԕHuԠA ;Iq~{3Z4%X1ض :=e.c7v.WO*5z}lp[ Zti.Y6U;M/a#$H'ڠ3`"\:Ƕl' x.`>éݪ~Q O>Zky#dj1q0@"^9`=}!fffnqTtg}'աU-}} ~Ĉ|Ehjaԕ8l䥍ai%=4hРA AR&L#KiC){+nTa֛KWG_ώ焌 xULy +( Ҷp3#^`x`y IENDB`kew-2.8.2/images/kew-screenshot.png000066400000000000000000005543121467402032100172360ustar00rootroot00000000000000PNG  IHDREb/ pHYs+ tEXtlogicalX2438x \ tEXtlogicalY200l6;iTXtwindowTitleUTF-8windowTitlekew-2.5.0 : kew — Konsole8t IDATx^eGq˓/IT$Mɢ* BF,4b1F 4?f=C,.;mwcmC3C3BfiFnn%d !H2y Nx8׻{YY,~SWn܈'N=q_1bĈ#FG?{;1bĈ#F|1E#F1bĈFш#F1b0E#F1bĈh1bĈ#FQ4bĈ#FFш#F1b0E#F1bĈh1bĈ#FQ4bĈ#FFш#F1b0E#F1bĈh1bĈ#FQ4bĈ#FFш#F1bM7n_P~V}3,Eo!U@7݇` Fq >3!nG?CDt"jޛe:Mf(upZ2َ3{j6m BJGOOxjd%n*Cp)34:bĈ! --n \I)=it:6):hTɚ:/_Qi.'T%4ה҈/F +EW_UhK*oT֏g9~%8Fu1b@4qmHX:74&uh.jEP5mtSjCWeN.\Yx_.#wFQEӵk AZ/ XV|S SZ{Rn18p1~obԈBOߩ}E>#eˆÌ/)]O7>ZE@!z%LesZ#FYBS!dP!zS_X$#+4;N꤭_v*(d0B TJdĈBǃRSE> ]a:B̘Hz(4^gTkh?hx+yU)9 *mQpd(YZD'M|UնG~huyTbsbԛ?bQD=OZ"4M"JB!)UIz Su6ECqPkwJ~atTۈ_TlUYL6<p0`YfM4,vS7$nսn^-D(2)G!K?>}!ɢyM\2v4{~5C4ٮ,آz堖WSģ ܬ **82+ T>m]UM(ox tr4aCqtN֗#z{(vGf}ՙtaQ(YWҏװ?e{nq.42ꮭ4'& 0*~y^+/u謃HbJI(jyhKr%2sgPG `4PQ|p_uCE3W30 $-b(>} D5 ?dI@bciPL>љ%e-f-噅0~UjYdMCaƱW%Eь&ރR@LmLu֞_!hf2Va$є9]p SmyDwG2(f$&ʭѺXx%$.v͍H2( C^&V(J.qc׻L?kW_d[ ?`_xr~A/o3*lW)˚:pk#` sTgHFi~{M8&cs\ yKuXm!>P_W;Q^drӯTAxVЙ_&6}c eakAp;ŲK.GX2>VSl|P^L> =G&H{x!  fSM&/2yy #?b(1Lq1X /'y4*9bMMѥU\7V_m/N!m}y4:i=e闞!Y|r?WJ\Uvsq`e)AoS­b㕋|L#\A E4Z<(,  V&Pnaۆ"h}f0<0 PnGž| 3B(Z>Frk⫺.v;|w 0Sd㽵`Jʽi;KOEB.WGX@v{R]F&s,n2π /b/E/`ޤ>G6-héɨnW2r0f< HQ%nfYMO@PHoҗ=,"C4\.P ѯ:qiȂ a[nBռCՇn!tX~̔%}I¤@Qz*ө„4Մ uэ[*n0q(kcQBH_ Zb=}o_E1r בWgw>(W3A<)}I_uU~xLnzV7GTK4=`nԈBLe.D0OվAeiVBz};;m&<]%8 +u;y檘q;Ј,vq&BZv#,Tk5 ¤; ~(|dÖܴ/ }[,!*;F[yVg:3HBWgrmd>7B[6DoBZQ cYcf7nj;Cg ;E%gAEI$8)NR^+DxK^~E$WMWyrant CA@0w9R$<̿;\&dP[4d\g4ľBA"qI͖ќ Ψ,MH NHrh}kmy)cc)AGSҋɫ=z]IN(p~lҗ\SzVݕ[ 34w\pp5E`ڡnJWE3~7? ;&ʭW*OPDLC$NDXV\* tl\uu{+O'}1ԠեFOkeA4$%]rImr/raVa Xe,Y{ؙ̆g OokMv& 5nu㯃!w丫Ҝ= T6nAޖw'$}!w|ۏ^4)x\lygRrG^]Z+)ž  X?aٳc%y*vi1}aN N8Iܳ/G>y?LCyPxlEyw6-S1b&vP0ӛP$WF*^b%c߅wNinz&Z' 4TΞ&B,]!^M0\Q.2I IKM\gX_.ڐzΒ,vMWFg+WOwe=k^mPB0r>ljM܍t7inJ ͽ_x{zku`KŇS&}WЪEQz\>=vP:JFO{_T c(ɼ5]*zRQ*}jY2 $QXz4Mdo;7lS)VTM&z[Z]bݤ RSTKSw ffs!_^]8&٦_@!̬2Ja"̔4 6œ"z/ׁͧ? aJy`7PP:<ʭ}pʳhYg/ Q'8j H*յbW 採Vs<BS'\/ɚrkA6Fl'k)@XxiM.jM(@ců=M"ޓ&Ė~rvܱ,,OfC4pUO2;PҗZA_MevIv QrWvx?4r]17{WI0Zm"-njF@m$zN{>$eՆQGÐ'RA7UTn-ުUrC5!:rOg&x5ϑ=MOyې]} 6|UzsAl)fuېUm D&xHfO3Ǡ?2ĝ#_gbޮ3v5)<;2'uzЋQ , ޮd e,zVLۑ8//pSE;LeH0k_o\4.0uښmO,@'% DŽcpV*Gb܅؁; ź NiFl3)#q}0?l}udhishk.)WTpژYcMj+h=2rO-ۿۡ~3EPS U}uāf)'"(*[tWGʹkC[r#$D_9jbc6F`;ozbf *uN6G:Xr<[ qiC `/cZX.{'iؙ5C!ov"m|Ėգ_6(1u,v}o5 amUg0Z=`fuXnٻσFǭl{P,Mˢn@P6/D([\#z~4V+&)0 wX'9GmJj޳Ֆ7)*'VdIе2k%ȫw(zMA&с[fyóyäݴsr6ǮAKY*.V3[+R-zD-]y:q(&pxیJͲʞ.J'AsyzuÂJ&H'ީ`܀t ٻmgEĄ(m) pUyuD7.DFM|nFQ er6ɫY[,&w':@KAoA\`u\aZCЬFOϣ7lK ؒ\Ižm6GoUe1+\)ۓq`$}0\49t!r#+Gl.Ͱ19fH.j"\LvS^m'd ѫaO]5q$^)ilFh.|][eVmciiz.Gl[H]}EldzIi{Z+6 `~PV{[̀Mhwq&.VO-DEFf lasFӫ٥iƁ]`:$2#3d[]; 1C6rm:(&]eV$&r^Su%> A"HMMzMbpu[YDCX- >nǿ -U V~ ׆\Ӻ\?=#)jNG$yѠQtƿ) Co7.1w\DsQW*֙^6>X9U65Lea(W4.+#gT>{.ϨB*Cs)N&./r]R.X[+ߪlI<}F[ۀU;n\ߙ=aEzm]j ),>94p2)OV uf鹀N[ GB(.Y?jWY|k=SI$2'reϞ ς#CT}Qɉ HC(OrhD!yJeߞz_Rb*$ͦ0Y*^謊?w_w_|G`SJC+f u3H}pZxJ&DP?Ά "!+0$UQ3&r7XbERhԷW]Y,#bqD~\2HZH2nZo+VCv*)5JƷAc_Ǫs~Y>g郀O빯mg?RxxkI2RT3,emlM{E FiLV3~!Z{xF4@emL^\6?FrY|2}dWr636[w_j Uq}X]/N-)un<,N0:Ũ* IDAT/BY{z{!ѥZ 9q3K 4s4FpeT߫ל}y&Zwv9}_E)9$>FEt+KKb|doXU)Τx)Z1GV&זj'AqR8$UTF$Py^2! 7ohY:JhRGjDS;MN+쩨vv/)$/Q/$fN˹ET\HnA1MNabޢn \# Tƚ3W͑9љ^5 fۓ,ޫ#h]eU2/6/%k_|NFQ Ѱh+>tNJcSB98jQ=k!c1k:9YJ˸h)zKo?Y˘ f5U`SxWqMxβ6#Gy3~,:2@{Mz$Y[>F3F|aD|iZ4M4 4DڋU_MYX'#[We Fa)_|#jem`r uYW܉&?B@hJ}yѩ֛(K). ydZڲJ䡖K7?~Z=-aaYMƺmEu?uAYC[y\9MO?_ Y.k}/P{&v9 = ZCտT&} _5jګJU(sʢ)e{@N4̿;F>eg{aM:t9GgL-Y _~h]wѼo>i.V޻8.q7iҺF1bĈ#\܍!W߈ܸ߾#n|p?wrFyq1#Q4b-fĈ#.=۸Fр[Ͱl&kUxYXE󪸛K4ix۔O"˛ؙW{]3M-EK=G,*ze|3&WO笣q$|/ ~ y4f;s?ޮ>k^gFc_inҿO碰!l#Iw==wUOyGm.EQo)bǙ8fNѝ#hĈ r8=&8v!DʀN"bJ4AW6+[IZ4(=ީlDžj#F8>7Oѽر{( a_NFÐ6ǼA><1q&lv83}oiL(><˻i`Ǚ3[/1Tj ۷V st[7~+[AΎDl @*vwZ 8-g_2%Ӣs7Fb*B1 ϱU\ORȓt"\E'j #3GMK9̑bM[;x׿Ef1y,޽ŏS%5~ƝO'I3owy'SvXsC<.}e hwa!ݺX%帆]2E.˹ 0z /4AIQvw!?҅* Jk].p?AEZlOcXEa?s yvāQ287?{bQLB-}1.Ц-:Ҹ6źiy4iSړK~_Du3x]v?bm{k? (fH8(.= /~@(<> m(R6ļPE_12P h"xΡ;_f7q_O^pfmOHr6*&o3ÐƵ)iOJc5qׁsuf`1mm +0ekO_fd/N.arprNhZx# &RL)^~Ɗ3?v ܯһheF\2h+㚫zTɁyW5 "' @!T@>Cx`r7ɿ?]{>~yȢP&՘(֨3/ /δQp0j:h75;7C-=t$C߬LCڤ:H&\xg^l{$C&bsq8a0eӔ,[K AJԱ("<kB􊨰x7BǺl_&[Z}Tgg.rT$T hſ"1[}`gIa.P\El=yvv9 OJ!ڻJ.yz]3i` ~Lp4q6C[B?5D/z93k V"w(ߘ`}aC3|{}S_8!˴sn2L|/Al3W0}hvvQ()"fY=W9|mJ5=67'e* h;+O?xB6,*Wt &7_\#MBc2 f$@Y?=Ͼr=4^@4 D PǑݧrO w羁Kr0=:?Eb _|l~IiސE"g">ڪ6@9|[ |MEj]aC54"m yI?~˳TJwA;pI3bn[$8&%/lnݧw^+*o? Wf, Q S~C]Y>?U6P~=E}p6whV ^_S!H_b\tLаL i()ɑgwpQ/Dup3'Hr蒷a[phͅxN,yp(%%w}У'v>b{KKQ7,skobCnKmw#KO3JodOMz':i&Nahxl4dt4jNYC1Tq?|嬆qodu3jՄOChCX'ٌܯ_edPՉa@*2zmP$sѼ}ma9]7FN[uw&e]NnҥѦY&ݐ8,\1=r7KF[~aC~2sښF3tU0| IGD,,AΚj+*vNUVҬϦʹ-e Ot>wWVZLY4%y^Ԏ@ tCMǩib[ϑ 2テubt6AfsP<`Eg7U#~.jCۇ4=o"cN.`ϟ~-n!E=? cX>kh*Yצ 0-/[TmN+3WXlT;֞Ӵ-ט;vkW"S^]ǥ\'!W_uu;." -9Ҩ.s!Nt6r>]c|qqf]|Y ơ0#r :څW6k~"T!ij"a.ID[Iit?g5nCۡZPׯQ<87:PHD;<;D#r׮8w Q8|WL7jx6R-4ﳈ\&$MvCs ^~uG? ޫ/RqL%p90E4gF\xd]k!op!a% :]tY"ę}2/ω?[nU+E)T^N-=h򳚷wNu /m_*(Pxi>*!CCaxƿ?pƍob(bD`ˤ8^Lj>\}f-mj'k*u%ծсL s`br`[9wk}26?b=lBʾxߔMMJbj$>{鷾(;lST_i5`Ze;$^?ٓpO 4k;޺so`1ۑsDCuy5TlS*[7Ca ݲM6X샽8=2o濸$woG[ Ϋ8]+gxw+ڤՕr8\CsbW֞?׿W_P|fEk/.Zш'džjtĈЁ|%Σ(8 qkCh\ĆBas8rgS=MD(3hK3 Pq_|4E8أ,=֕dhp`^nGԔK :f5e6;<G+VsǶhD@@vl E6M75y~IF=4?jkvSGSӃɴ]L|&P ]Etg";WGhĈ_cX7,N7ll4i MCg(8ⷛggؓ`/\a{qMQ# LKQDo PF vVW&*.]_!{h|Or6'4A2+xpigl 1jkJq!vUEXk4)]6|9|:]'6o3#6lm8G֙]:r^?nG }h+2 [o@Q/gٸ,U(:βO튒tH$,+&8Wm \LCMt7|E7q X#Hvh{vVqZ|!4iv;O=tPQh_+3+.]]eW~"w;_ukEt, hl"0M7J7фdrl~1 2玕CR^?!bgyaO^.\62Źn?^&1y=3D⬑~tVy_aMjh^Fy`3P,h 7$/#v(o]W hD1܈3{%./;brv=wu!r˫|kW?_U֚f7}NX_r㮌=D=w\ݸu]'M߽N2ż;6 䩄[1{ @ 7 i%嗸u"d]J=TlZ-ciC@EJlk;d{$6ͷN~_%;`͸mi6$ul␧p?6 P5v /%Ӓ!.[H_FY!e:57,Uy6DZx3lS1,EsRw-i禈W#ᅧӍ;i%ܩ1^ųa&n<_Xw 1//߫W ^2o=~e&+y{3`޲璗nAhVp [*L̀+% |F8^rqPMt`VH<~+zefX/!kט_yݧB(ma{f/ CͫU8l֫iu f/}xT#")_1+D9|WorTD$ Fffh~͆`FoE2|Jٗ+ׯ|b4޹=:wm@؊֙=Ǧĭ`A2R ~vV_ bͳ;TÄ6gVKq7Vigʰ:kuS2aYܧ_i5[n⯇[U_M ֏.j?0Z5~ _y'f ~ɕ+Lыl{@N9MUhy"BZ$f4weXoCYYaJu-KŮ,rkpj+wIҖATXتݧ6 (yɭy^[>T5'd7#a ;JLhDvffl?Cy>5y?4f6qZ8YpK=Ml*Q snZFnM`\RnsEƙt967DmI5W 6 ]uܼPݜ7ݥP[Ϧ4su`}qWS.uIn.=Bjy4CW]wPxnvLí F{9Orx7x^St-`b3WMĭspjRm)6?xL:N:z~81{mD֊@nPH{/~O;H5ʲ2I<DцAC(<\zp}Y斊8V: )D勗XxS7YO>I{'& R%C%Or}~OKDmJ+?H=|}R]70Hgv^VAuVѡ/ac~7xdI lj =Vps޳R5nCWg.ӼI:N_jmwtؿh= , 5b#­.}]6k{R᥀o-Wv:2q~eJWZA~BTf{ %yRg~&Sadȃ׺Y{}p^N˚4xDM.;qi654fֲ:('^@0O(u2:)ʹy<_{7 -t޴?RL!ۍ4.a瘒V8>?)w? {{Ƨ}Z[Eh6,!`+qtڸN;Efi!0 SԶ'֫/";Zǒ-5٥x|Jcqyw%C1^fkOT.r_@3wΎDm vԾWlӑgNL+ͷz:>ڰ٫`?8SZVsV b6L-CuqZa].ii6ݳAu8-:g8b9y3"&R=*C2kBU-y)USryp_|>bl>]X;Q8rfVP7Y{RGe[1}jutC~~5i!v!<)4/i uY)Gزa qp }ikZ޵6JQ b^'N ?$Z07/m1R-{%;jե_MT7s | (!znCk~.SްvgBv̓^xWv*9=g%wc?6/r.у!83>=Wlc76 Z.A!dC7{8PS3d2l &lc^;/3MOk+ ٰ}b;_O.#H}9ukWwu۰Y8ܛu_ړbSZ֓W*κ" ׸h\` '/{̜yXkĬr.TO pq_WʿW^az2egN%q]?o/ElmŐ/_M-konU`(l*m۫Q WUڻLҒo 8lSkg)[E1dP:iUQV_ayh6r~k\gR9 lњ毽ku(}g݃;O3gDGΒIFEJf-#eVUZ:0VRE%y#J^)ZҊԲѢ)0q1҆ a#h01ִl9?~ܾ}vO<"%z==%+it(GL37J-D$3Y Ze*U!8P1{vSG)>ZФlX2.yb,onx=O|uH&gv_w7B%*t-KK="+0f nT?".$o\c/`nƿ1=G_%^B}p*䟂#1iuL۩pFB`YK僡'"\)-V4 X c=E+C )9܁@$C i]L'̴;7Yyp.>(Õm3}Q&Ct'hn0!څZQݵ?-X-mY'+UA_zSeCJgN2Zl_mͰ \ADZR *ҡ,w:Za9+yS;\%eypX-4b''1oG#*%S~>BefJUV|W0s-:E4eVAg;|9~ُ?ugAGs亨;i(:Nb5rUS vA0>U^,L۩MBBRT$>Wޅ'rTW߸>EHʇ]YS7pCWV "&\n>!,&P[8qjqB% #۟yw\hd[I&@/1^<upmRl}j8Sc%h%&.Mv>W&D^tkzƭy-BtlK!~FwȫGW.,^-ډqXw$5/CMGw(WGQu|c$~ܒ'D|l VP0-7h_n@0?=CyE J4%WMDS'iWZ8Cɵ OĊGΉ0;w@b7#S{%cbK@h`ּ`_?EлeL4n%p:u[xg'gQV#+,˦gUGZ L: rS}hFLaIaS=hUnU3㊹ٳS&tx׍ٺ;:.SX4^i!03w54ͯ%mp%,b9yB7yCidĺdO^D^W'W ZOru?BZ0\8( qN3䳔[+fJw.:~gq.Xykm6? f#mrV!oݨ*kmN@i{1UKBFK#6NyDtdn1i'ohK_y$xbg>/{v\`4;KaC]7E));h0:}*A J1%z&+ɵb?V l 력\)J=,зOu3UU"5aA=|p W/mǭa ὅ[#~d`7SXCTuqA:@#\#JKF],'Sy8s |V q6\O#;PV%[d}T܆ Z ;펶>gN-o"+u8l@;Y&H>՜hޅRsCH/2 h?4cGP?M#l]k'ºz1SԈyRTАˎם<d/. jb: =hkO7V>"[>f,70%>К8xZl`YDl{jL݋}4' Ǝ"r p@~6ePA}7ِi'Bg3`6^D tXqQ9cv&4..:8H0 Pͨv;ژ .0;m[g? Ŗnȵ]дi6E(Pڸnkk@ B}Nc]s3JUH' -im *[psovȱkX/[w=ÅXSS'Y^9aϽzQxw@NZk7Sݹ^6_FݾvSf͗Nj'cBas݋Oi(#^ٱ^U3 NgbjK@4]`<[hWgT@ [?$rdٮep yz(@!r] !}*Z) eksB?S!^&mQRÎ+4:qc͒|̳#G,JP~9/?'Bʇ|A^ _2ׂ8\H9c!T+/1[=$D'~i.+8DJ/:i'hO8+%)"guqGوTz &3\镓WtHt,{sJ@n9%ayJ?"ŭSjt^k`R`#+!9zB fLpK--koQ0fb!Xtgָ 5 :- !cSS4$J iPC ]4L"^ߛym{[:lysz0GM{As1U7n:&l#>yƑCr׆4Gdxٰuɩ yE ?o!5v.PUs@BPOW6 zCka-/ t*KJA)_OoӎEo31m?g⿺6rPPd]n.(տG6y-D|^Ioz)#pҷtno(Y;&2=cm($eႥ?=tˣ ]iQv$#?(Я~5K !TQ.|a_]W+~šic%Zʣ  8U]⭛r#i{((h[/A\47Rܰ!?Rvsgon)G)y}vwڌ;MܡÙr\gFeKJQɿt EhkG|͉kB / O)--;j'vȣjbl Qc̟|U\6!UIzakq 52.}g8IIղ@t>84C4 U$Am6\o)8+т9 DS} Хb~uv$ %+&\^W压+StYaEʅl×aaHJH{Ŗ-?yqs&1w;-,u?Yg۶g V$՗^榣Gh6M;wq@nLqOP[!8_zʿhυV^>JʁƳ{qLPdz<i ^w> ̾z}c[b -\md(:K]" bu[Vv%XrnPyy؂A)1acㅨ5s9STvlC Y'vtg,M͋`BPcvo+4&j>*j!oLWXrZo (JwX]1 zGJC<=զ<橼tZhʄ*ph(p|ug-%#zbgX ‚K_ͧм} 1T\| = χpAFG[0($fLC>NJ 3gۿ0<8nE1'V8ю 4uG};C/z5fOS[e nX $$@F_b?)Y'88]ǟ|z0ʘHɠ 91X.wb k/ΑmOLh%WZ卽."5낫p=MwVQtجSKky?J$Cu&!"`F|B@c/QJxw?(]>//)|lL7fћ3ޖm@S@E- P*wq~6O/ E[CM,./s~򨂴o-sBI*M^ ץK4n5:._"ٲVRK T[" i]^{?kc*ė]֪ ]@4q}[1fgE Z\-Lܷ12v{Σ w<͓'6Zd{)np3~M7ӾhVZ[/g,FE޼K?˩KY䕙d×(y[ $}uMV.tx@bX?DٟVo4uprtku+,%!MhN~\1+)Qe44/=Civ@-J;xP޵[`tiiXwֲ qu\ęP"<]!-ǎ`[b(ISoo"^jF] =OejM1җk(vf?s_y<% vZ;HY͒!6r؋v\5ٹf;W5'2vD&hfU.@6O'>6ҬPo/"nV;7dzPBL&hY7c`\7Ɖ$ޠM}P9W m̢3qDٰhG+PϿ_͗_,cg$7aDSCLr;;Kkc-q/Lѥ@p!..~ȋ^KưN.<5T  hU7['Z"5Mq&&ʕj۪)5{z޽̳{p3m;u[`\0=2D'[)LM~pdj߼˛o^X~맼!<V5O|nc }FXl*(pq.po4j͜&eC'Sttƶ P&y,qE-(ѯyuK7zA;7faʭkn̖L8Wٗk\*Im)Csf;MZRi|ˬpxg+F+1lh^`W h'-¾o4]Ɓu7g<7"ç.5-Y[%āWB(D^N+0`q73 d !SXm>YhZ!-X7;H\y# ߢ8g9pwP iƻ1q |0$WYuxEL7 C,_ReeY t쵲zyUB8;^oi$?Hkzu:zBy ddgG,O鸫 B6RB:}6A  B@qJ/]Wl ǥdY#V!M7h%V`[9`nj񇘻\`ɉmt;(ZM *bKMV%_yje+'_RHTٺws"EiQMGlX_k,]Xn7m4^أ`Aѱb}q\ S^}_4'ծGيcY34Z!" f7=Cҩ$a+Clض{ :EF3+cK=vg?}*H2dKZK+ݗ_=5rwhc<)YO@9mi~>r—EGv ɉ2DenTjF{^p1q%|\DQ@y=Y _K Q  0@Yaj٪@*ʪg7 v Kj$Dw ذ}m8\ L`#gyb?d#0wtSaCrT~ZwSu^eóO?d?PP$ ;oa݇+%ntNM+|c͌*2JxYm۶S4'Jb۾Q0llf)V>+kx1Q^M_!'AL]9Kt׶dM|/~}m%!bDd0N8Oj8`g/ԏ} ؓocLd>vcpqѰ/n<w-nKyuq䇯9; !t+poݑ($8|nFm,YJP^8Mv] cRmɇ0;%_pʁW{׈(R(0= \ظT^ShbIy]޵򇷠E]v6^ xYyZ LC+)Վq7S_K!l\Z&*e ؃/?8j}IKxFފzk4I ԛ?ZB7"6bPyW"a뽚yaN}+%LfѲDjss9)N 4lk7']H=h+LK遻C}DI"gJ?G4*p߼X ,QH?0ٿtWZl䗍S~X<O8rŠj}G$`u ,1!8uTIU7؎ VVP|-[#\J.˦shiYpayv92q1h/B ?$gcg0VY]#oG6X-[|1[Vy9yV'EvGH(0s- /cq( 4A3Va$:3/͞V@ cT?P<.a:s[> wسNp2"7$@߻G>g>ZV09*҉.\ڣk=M689aOXd-K$xEa>`A]R6x^,-or;b-7ji.p.|css*UnN'b%^B?B3)M T-;&":4 h‚jM^=LTرm+[̇?ڦ5l`t _krHcԱx`y#niNjR,p: |f{4޿ЙkX0X>䐹MGRTwNNJV䒚ߢn2ItC?9i`{mC0ך' ң M(gKc-&ū]ņCyO,"\&Z3LܕBhi rS|x {G mb;L;!ZN5|Ӽ^>*~{R+y~tVfmep=o9s$ ,}8 /R/k2K!Eˋq%./] Ѳ>6n ̄0W8r˯`[_~}wbG֝? IDATY$Z1{0t2\(1PPz~x+ 4fIA1/^="兣1ڄK_ R^4u\ t-:הAe4訿NPϾܓ_y.K+WjVFט=~o~8_1n.yܹsFk->×Z jRkM O#'Qk,G2V)q5j0(98٫rIjk} Lž]jCtXJ5ƨuW Hf% O? zZ@/AtqQ.ݷ=(J"e!QКC7ְns4WB/p()9MyRZfN;b{'%QMD σ5M7 igzA f6xt8o/"]RzN2~m_)9οxߞ8P:lQ ^KaEz\Me~> Fty]I8IL-t^N셁[(!&>gI̬ʲl5[PNQ>"VMg/)ݷ ':uT*59$qB"j>.wi0XN?=}0l/.9b@b%d1nX0"|l,P{QwOl/r8C*5G½l6TP^8.h%B4[B׹/qS|sMft-V|þ_׿ooėgߤYۜ3*Ɠ8r.Ίj\X|g#G{Zsf޺p!e6|WZfFMƅGksT#5kkr1l.F=.LQRc;;;Mh5[IkaTto/7Qevaahr/;=Ù)ĎXT´E,Otr69Yӊ{[Wq f_==]E|pX-N%~D[O$q $[{ȋ#ȆO/:qv~XHKA4#.:Qmq`:Y<âQ 3V~uM !t8'zX {hMNrl "! ALÓ${27{lrݼ|ߩԺ4"@8"U$=9N 'OVcxDw=:7s >H  `Jõ?>Ͻ7&oClu]Q˟3viAsPy.Hk;UZsܸzGjhS:qΙSOPܷi2 {sFWڋ|BM ? 9U$BLUmpӈ='n~{c?!j&(fmHD"v[+s^&g__yP^|wE11%Bތj h'NAbSQlb sc~zՊf ЊUJ 7`xֶx|}͑G]Nyw @Bd{V Tkrn-tN`FP;?W3<[|cs2d.+}^Cv=kQ>UY2H?~3/5Cene [JLZnq&jwOB$ v*/r>5bE\pS4|/ [^;=.h'a, 6~? Ms`• a[VBUE3l1XCl51sҳZNgډR"Qy$5<-\4˸RI`W*Kqc* !P"!i"'!vB4AwG%} *rQ`+Ҧf`b2ǫk׈18NlM&&FB9{|"$؃/:k6JRxs {o?r'=:8u zC]l\WWQte>|婩d)kN007R`ƫ9'8U%><4| ( w<*)mVԟ?5a!oξ I!c[8wN\oT&x[ÂE4zV} ad5D0JKՙW*(ɜ"(_(]8ʋfuދms ZB$6*<'ٷi} 8@%t6܉ NNhNE^eKQm|E엾&!By!>oT0`W?d >9Bؕ[/Q~o/ov=ŇRkH5e+G0CgE-^׺,Qʛ6 ]3iW E͏+q~I2ޏ MQ!lQspp5ú;62~z'oDkCepkJ&ss^k:9 ʵߨIڧfߤ5z`shoʮтA;-%]~yӸKSo߯zX\r,ã g{)Rbj ftK:}x/D VmGv@mƼ ڍ.y hÓ_C(]A#ֲ5X+0] h㜄敜nPb8k~ejݚ/'An 呍/Ay jG{wP޵ rw;rE˅CJ^UUҬp뉓x *] đ¥n>X-q$vbihZK;(S-Z;9'忴 fq*^BKEQk ȽAO|;oO/񹩭V8:[u=UmC<*?Q8Lܼ8ɫy 6AGʡX4jsbOE\{(9SUTR#Eh$:2Z(v1f5Nf=x6qW#`9aH@&}v0$(f ',;:x!f"BBXciHQ,Ԕr}޷ުn~1\u{ޟϯKV`Zaݛ"7kP(f! ?J[L]30=|)+t-ͺ3E>cfPU\סtshϓwvq˯?q PuK}y=?wrCK*l_;&Gʉj)jME%S9x8?&rBӟK@BDDIB%}NӠrB\H@H' :(8%)x(H\ J1d{V F[TV=s4fPLگtD&c\BsBO:ޗpB-{~Ms! biQ];i?-B32;w*O%٩̹RZFN|N֪&|Α9- *QYsc >Q d`2tN $r|n A'.Y!y06akyu?B*+5OSD%NIiEȿ1oYN% ̆EsٔdJM_2"Z+D=W[ /!'ΜsR6O6і[Nw ♠kᰈT&R?^#Q95U@b,@+kE(oFax`E>j4bL8Mbrx--<9#!%OѸql͈9N(xEE}^u~d {f($pe?Et:׎{?"eэ\2g_:#H5%x=:C- *8yUa_hp3f#GBh1}CcbhzIh%\8fU3uCv^"{B_ hX^6C1&ɭ%sOK>>{<5#S3Ս4t@ۥFF}n(ʂXEN[]vK :@xRa,.O(6}bS B~27Yt7z0K]es [ɣ?=,"1_ 7_1BNcVp{&/K3m=L#T_C0Nhkoo8R%nfx!jR7`-]fNoh\ze3v}B_و. Jwly##x-T1#}L]"gƙ:܏){jk6kǁՊ7֌rA7.;U+bgu@`k1$Z`믯1;vb^!e҈:Yzu3X]E5[&ۗNWeD<^A#Y y] }c8Nj;{P2F_-d ۘ8fPZط)%UDp\E$0M/>iъ_Oc_J~})=z?q 9V; @1훰kV ֍YJ:LEnlT5p<>3UB4d֎dljcC %ED5ȗ~]+ qWYK ij4h'0XjG/~E+eqxĕGӗB@cl dq%#?h(~n)[ꬵ=Pފ(ݱIOlj\3vQfBnByCl]M δc_} @Ds|\&Aw0Di2>{f*ո㞛;J^WxGlQN#gX(J#RYTv$)9QZM/f ,d^@ơw=E!Nqq5 >p( ǩ=̳߆48/R"251n|`54I*t\e>K^VYwC޵˦v~Az Kd-(m=9/N<.x% \u9=8 xX@O'~Dn͛ɗ;`'H8T^g)X $iM!Y.!xm!ƇZ$ ōѥC'Q@#!!zwͯP-u 89y߽2ZSo2\XRz-k[=ϴ[QQr_6Pw}nlAxvZ 8Fw9I ; w'|=o6O]),~2% * Le H.a,ci2(Uf{AqN;9r:`$alOXZz7k2,v "r9?|sf`l KXBtK$H֮d?!z:/'aTXv"DS 0)ia-?! uA<9-:ne4}6//lu!v7T2^'>KBk53"HuPNoZW`|d1S>OAjQQ#1Xd'P!s9 ډ1Fz3d!9.x0>ޛ;6<}6զ^X߁) w­% `h/|D;.^] +AlG[+IateOMށ-Ua[/Mp* kQGoT̳Xk\*/S-sM /}3K8n ). kIp}(3|P˵#@''XV$78@"i,+28?qZug!w^I_ۈ{g$?=À՜ +^ZMt 1Oꑽ7\K BpWp/I Z]u K,eACVCkHB)rWɟ >y}z3˳=}3^eSĀU(0KIs=u}Pt*gv'^yW\NS^T9G5:\:ZHx_O_!_fsZg^㙳/쫯5\B+r(/uqiO۳+?MQk $nĀꛢX̢Ԏ|7Ł jB>g?]5_:cAuW~#$dg e0w0WxNA3"=-׃_ g /w{ AzOE[3+"Q2;"c 8 ko ^Q'ҨsחS2а8,9vhM)랁G8W{iD.$Vy$ K<6r>:=v :jZ;z&!efL\ ~o8ar(,Wx4 S튴j)ITF-pDl~bT6KaZ 2Ewd[D1K^KDvy1^s^hJ!pzut% 0ݏӼS9PrJ"''Ds>N;%,A'|jw?D;Ğ '^m EuKo('0p2EM}J'[2r: B9T_-P".X>z3'Eqy> ވ+pxHHѱk x*'?#G;C #dk-CwE}tOSc+NBv†z 2 4)hJz탌bdefRߦ01]TrBT'URF,+nm-;]P[Ѷa1a~p8b589{ʎǨ9ϱWXk=5[D<Ǘ'Nc\\Ȟ]cCٽ,_۸t?@x۴bu<-D?{+LL³EaM:qx^-},i@t NSǩn޼pP{9 ϔ bP6F;!.M b-R5} +}53Et; y;O\-]W& cLN3lZZp(tv&Ӡt'7z h,Ƒ6"\q6fJn6\ l "%hoM,΁kK7^7邡ҝ\򡕑ʢFV6]B9]b$:dtxO1[ː=ښW^1hI~n޷8}g&iP_?Om(.eH󎺆OPk%hwLOi|ik,˛lX>ΉM>8 ut9".7?D0L[8]L?]>zj N1@pɺ0N(/sGڙ>ysܹs2х^.ȋwa˸Za81NIK]vAᐽЬAp^B>g?(lXO}{|s[d-R&3FK40G@s12wURkꥌ׫FR#}6s}on佼L@Ri(K&# 8QWrˁ=޾C?322&QiU4% 4728dudjqK%Ɩ<3҄HxɁfG8;9JnF2NU>uw`ǜ.mۨ6Ϧ^vt`"ufjO-`]32ub=bа{,YŕHVl.ȁI@q0 $Ʊ}tZ631"cm4Lw–boww(Q%GdCQ+yY͈BR*lduiv'?CX >p<}0h LkBW3m5kh^+"sB}O tB_AԎdўsb9_(lKb]j b{nt](F:0-(IyZTy9CwE>޶YDV0[u׎ewR{ZH4"^FNV+g9|qp˻Q;#Qkdk;nRRi-kW6v #+˔ 3tD,h̐o9ѳ}8,Tzi{/o-nZ^tɓlSvࡷȵb' b=cggOMSku4({?)[V*#َg\Lpljc-c9 1<;S1\@#6S,CpXy%sQ#c ]?D 㠐ziV[%E ӭ@q bAv̺E۽_]$E Yt Xt/ʇT;FD!|a//V]._O :qM/N,ROx"C;bhT|"157bu~t]С+O+L=<;K H Hc.Yl(KS-z]~/mw"W9%oZ-vAr+zÂUQƃ(>0*WZ q[ކ(FT2 j9X/XhtW}toW^I_fԂNalY1U\ MzP/vԮj}mXS?@cul*L= ߗ)Tѧ9*(s7OR͋K?,^2p.q|6eσ<=uCq@$*˶%%~'Qn߈_"_^6!B,в_& */ᇸvkL]D:*>Bʂ69pZ d|u: [V*.ϞQEJ`xe:ch=ӧ xF"[8jdg|h_o\$ӑ$ӳ=v~\W|>L GSϟO=V;-Ңl!eI'"XC^eڏGd}e(5#^bRr^*,&Cݝv|)*V{}?3ɣsdJXYcp!fυ~Jo;ϟ,ڟ ;ބ1 !6&ԧ%ɗZ"Mz}qbhz)J2 AS'XyՇ5sE(<(EE֞O,r)a.@nj;Ⴘzm;5j=@ ^{l'h<3`P̴A.zI)F֖噝J҈(ɢzaF<zgł)FRG㲼j7:{M|pdY#ANTx{0V v8F#ز,I#Y# wfZ?ev~rX#m_a>Ff<Q :!R^`z`E]VFO%֞ a6BVj{w֎8CѼЫ Ct~#bks;/Ԍ$Dc58q&\h4ds6D06XkO1̓(\ao9ñS>ate+n N֞E P^[ 3 =sRm5CTP NU)ѽ5LŠQZ|tBdR|Z0KW-RbmuXog6{գְ=5⢉$k{y$QYz:Mi|/ -16 H> U&90'0چ(AP9A=M78^VZ9oH~^R''3vq/;41BtkKUGz->(Ct7VR4v1t`<41vX\v8'){*==1=%u7|y#HJ˻3BYX'!7ÓE'˼4cm )jɰ˓ǃ,0GNg6b|5 gxs1F9֊mOTkmқֺ+vS#.Wbhp 2{36-_R D H|DWp-{OF:\{!ޛMcC5B]R穹iN? cL|1*%ܷ%O;4biJ[ w2!ehog{ꖯt^V(_<7b ouG6r-{=O6,éW9k9phӬ)_7 !/=4Jah=q/t.g-Obw|3ˈ^OĈEʥs/MLckoB@Km|U{H*vL4cҢ5Xʰ@wo=wGf&x5lCqxٮC{Qe+ AZ.AJK lnO"69YPV6MS|v;[1l% 0h7E;b^ɨ)w*8D%ҔW~ѧ#7#U^B {p2۶5DvyFL0SiI*R7sstq42 !WFsO92s[0b@/:_ŦE~˻ px4V{o$ v8ՆR%OׅB$pW:%ZW bJt(0Dc/s:1Q0QKz}L.JAv 7V-u Q~/k( ǿwA9|]RPH'QA5[egwX5|yxxr.d1ƖgomO'(B# IDATĖhd8f${?8kb><&;!c/FٗҞ윎Q+CSH?Fc%J~YbNJ~ _'cA{g /Sq"t#B?yz9/! 7Mvp (K+) Ҋia"aƮ.gls0= wuȳvI~\w{~[6D?[N{id!۬]3deA٤58'dcC`ճLe}UK^9p=ˊ7ܽO{*>{V*cR"F9qϤAk="{aqa5:Kc~ύ&3"ghi3XKNʯ/Qć0nyJ1dsVvOuFݦa4dAYD6gW'Rcs{xUN|,` ֭6lH >-:](ޫ兑/F2}EQe*EAC$S nѳ^AE3p6cVba]'DWG˓KPڳ:{lXkWo iӻ֮i &lX" }(Te~h(A:Ut]-֗ 읩b˿.? .Q>ۦEBg56k{R_aps^hͣ4&륮EDZ$B>D[gއXG*T L܏9\0΢8c8O]Lᗘ0(ZC݈0l0Mbc09 'WKZmAzp Q'[<Μ/vUz*4֧A!(蠻`+,t^L5.D5R NV?GƆ9ٗid:+G;Sdv<|ˍt`FzRN;Ə";Kk6G)ضvs 0 *[ˌ[o(M>g^G-{gǮ QoQ푬׈H82oA5?m/,͏,Tp`ڰacGGz-չ^9H7 ق o͘{7|۶G1W"Ua5nff:F70=9%?U/OTE (oRY}ݔ޶IӼ1sF95Ȣp$rӨDg?dxܗ̈́gi #vlqlS;R'ı?F!hje[w^am%La$݂eJ#)cӕ 邬 f='l릮]K좑^9\c:R dӆi2Gi9pA`89qGi$ZM6Hy7 V]cP>Jv.ۏ;!#qvN#k0]kWT8ͤ MEC(UkVC[B̓QqC5]נ̌a|NvxD{YF6L5U@a2]3 V2 ܊^!I4ˁO3/Q?xdyB)l-wC0^ ЎPxWt=B>6W?~"nċB;.@ao]Jw~8NvQؘLCnSP<8~UUpDbT8n} {n9|^"]axJ'(?Qƣ8 c;' ?REPi< ,v%M(qWFJlڙV,]hYtˋQŦ#Ɗ\y6Њxm(ѵ>5scB>R!ҐKvt5۶/Զw6 aCJi XT'P g(+ hWeaʛnF2.IYI4c++-띶 "8޳bo7U 1w jȚph8}*d!i2-FV30 CX׀x~V,  ʒA/yQ8Y[Klb`|L˸3 XD13cx`Cpw@6s, )"Wx-<:l n蹲izઽxb] r=D+?PM6zĥ/>)Iٙk0Hi(2c _o`n\ᒢb?4}w(CsJtR |3RzMY&mak@P btA8GW!xF"/篷,M%52{,=$:l//>JܡT! h^R) Yg9^Jcx*L Ž0W}b¨3[$R`x v+@w<;;\x a}D2+ZH%U='#z/ՉA|waw.?Eλ:j<7|1S5Dqז%ƑJ'WzZ݌}rxCv֝v~n;۟TNҲ. <XF5VAu'de_ ޺[žnDĈj@(a8@!nAmZ|dl]6)b_MHU B/2=b ]2Ew]q}X-ẍ́}=nWwc0ў>0ܓ^FS3P`_e/D6so(X@y5wlIhN\~vB.nBwIs]'] ńUik! wE>I'=fͥkuOY/ )C5" W x p{S\S];%|F.N*DTO)O1dBT唁[P3)+qit5 RdS>03Ėy+ƶź<âR8]U玲A}LYѮW7@Ε?b!YE%&#o6`TiYCo]vZ$1pߴ 7W;ZNbhd=Qeߴ|jP5:2DWV+j$գ!:DsV1mv`t/c bM{BbH{U&WQYI}b.Jk1z'a֗R]F)ELʋ{I[^O=B轕טQw"%c{Nlz:o;^h LJi9)#ӫ(M<Ռ@]i_x2H E&EEqԨǾ޺.fO$fOY 05,}F"hZ>?8-%Q>eERTjymKe[ D{@6-$oID۟vs|~o3c'xSٺY^"TJ 'q8ź ǣXi;cB] FW;%9 -eV|3Ƞha,4-jIKm0Jq34ͥ`#!zi%AԼBv} i NUE hIo1eb:N |cc0^y3mq5|ƛ[_nF֠p֊̌CT1N{sڈD͞Ԉqu6e'd5oDz6펋Nswsz(8q SS8@+#< y~Iәhqɢ1q0^Q3ֻYjVS3[+XF,o1ʼ1*>_iK|Xp80כҘHKr =_L+ү]e da֟EqRpĝU_UZ<N;qGfYf] Fa@KYTO>W*۹-ٵ4k7x@ WGlފulO18x {(a3kRAal%x5 SxSCЫb8 ջncò9Z;)7ՠJ_7׸i2$: Ci27U^5'^ƋQHޤ巷!x Nt+x+ݗ}/h۪E?9y.ŇCpMݓW'Ry3qx<1脨 XYVC >pO*HM{xGںOq)'I4OL5ݿ9)*Gl PC%9+p󡬘H% C9y@+p3(51p믑LL $IxVjw IN|@..'Nr4<ë~s*Gqu:vo쥎$40|F X$o_UBY"{s۸f䗢5poe\z2:1I ńF-Gc4aשYo_È͡"УaS^oݭ=S0nQ$4҆$Gf{ռ#i+|y 9ť8!ԩץmRyv/5o1?m:,Vqn2E!^L8LLe_ZAaR7Cr9pD&N<|Bb$א)r $g}\Gy=U}zzzfԣHcY-KK- aʼnkb xM%C%wo~v$oHq'$@cE~g,![~39]UTӧG=l>9]N/*]DlNU@rϑ16:#up7ʱ#:#"cyWj)W<@{O`耊l]6:kǜv(\`Z?.\T,sqq1,bmX`*4!>*q:Kg_X_^geeZKсq?Jh/{r ydۦ)XF)P4yQht|sT)gu O̗100>FITgB_’Ar~rNhnuF+nbI_)B[(@{#tPb 0Hm`U#AQqdZP:g>^Ԇ(Y5]hRO$Kb*vj15?HDr)q64rch62_R8-ڡ4di,Y#.U"IclナpJ 1ͫb0 dCN| V]_ݿG-h-Pmrb0Z UoO-UО7ivmq Fζ#kǡ@l4Xdi12ZBDldh%UKY,Ksqb%;"}|=g7韦swu]V Hdo:Nf}WKE9YpWs2d7~+u'QrL /R IDAT 4/ Mw:d qQ=RAF/YLHd毌D)55/o|ٿߌ}u'vx_2@Ф T@=pچ$2(^jYr N{0.áj"WcvA&>Nc`owa'V3鞅dxB! )t-oSq8׉8az|'?rE,JÚ<%wEヿ'KkCDgr=5ɇk;f*O3ٹ>\p8t0՛g%9vXz5f uJ!REPzް&,Ds9Cƛ`|,aj0 AOeCa2.!@{)\&+;mBI 7ShrF[E-9pһN13䍦U[DZJ"fB,_ƆMgs ͒ y_R54E( ~4ܥi'm;B/E|sˇkwitoI0~2sk?$E+@cr8j=q',]J@@y(s>Ǿs'aDyMPM||{已Α$44' S2+.PP$Q~O@0i9 # ^u%< Z @NR ʩ7ߌ96-i#sBMP1ۛ UMSKЙM^D _on1rYHLDC{X3!T"Xtؒb\",ԠQF}z`/m04V|KYƼ*Fk5=0n2{Fîɦ bTj! C|rmBS#m}嵲CI>:& ÐuKa~r*Λ -%"n Y<N>iYr%4|^ZIV'cwl-P48=O-2-C=!_ ΛPVvse#Y$ZKQղ{q{k凓O=bn;7\M܉F Z5G=.gGLC$_B_}'nG 9VZwYzԱp׽O ($I4CO#pձ,sד; hDgK.@a'qҳfC7z倜 tM;!oL(Mh:9pڑs(S؜AqN~(2^It  )&͔2:YMQ8%v0jrZf+?~t =%f]]KBR;40Xn!8Sjr]W.^:J07;9>#Xn-wQȖ#misv\xd,:$95˗u@;tErۻC/4z"ƎP=xR쟝5+wpvK7dUgou6- u Hb۸rͪiI H;\oG.) Q0Z8Ś|>l.]!SxmBoofMGA&fKy ?LE;ԡe7Іh5DҿҾ׿sAo :l"Y\ ;V;f{q5}̬4sf.꣐2AsuT%plrtѡᇝڗUv~O|R4E/CHϐIFԄ#IS=ҥ3”$ٜ0qhQOj;%q n3pu y'EA\Qu!:i', aPJAv؜#oW򹿻7maѪb|=7 bf u sKcε XI]zE c򹀷Q#M8@ʌ[;b'a`p"lD9;ǛZ=N24y޾be0ڦ (4oX:TЩz01s 3|Gx .XRfk*B˜Vbz3H]Gkϣ?@-'Hzf L?&d Vtг?Hx9’xFQQXoޏӏ-bўSs >LNP"A'BFj9Gif,gɐ0s#Æ]av:eM~T#v7]cGpW7$3&{d#9 nVyqS@A8" αQaat\;ѿfXZ!@>|w5\'>Ὶ[gc_5y[@YlkM,x|%85(PMx?[x}9#btO*l2L0RؼsC{yX"T.v>ggmj-sƔv"r^RHRMKzB/}+ zNDVwɷxpq%r(o1Np8{B-]¦E+ i^8Ow1eA8 t̉w'v(bsFByУpsP;,[\>Cwee|Crȕװ)Oϴtբ[{(9 .^#Qˮ#;Tf]Їӊz>9ª8,<$  KZlŠ6yL-SM2xI sW2{ VP5߻W_*8Z ̟W ,\(Do:wqc~,z|?(G L -sVz1D5# OgڲEXlz(^p!u=`sbDZf?,r`4:xГma螎/:|{⓫!#o߷\|* %N$0] G 34@-R'}͊KV-j@lt0 7va@"ETqidrtizɌp \l}2k> bZujLK^t@lYYy1eYϏ?o{</auGXWzqfV=^7wo\AVA='!cߦ3^B u)'N%]WԵwON,Oxr<V/ /%hHfQ;Yf)T_]*l`q{H]V1`aE>L%e~±O}ܸEj@g_,%cIB7N 3Ҏ@\ NN/ Y롼FU'3c-Z{Qݹ=.VCJﻖ9ĸUyYw0L!_47ɱ}(HKY;qȊ%E'kuY0ȱ^lnW׀#w0?kO^HfYZa?j._e]qɘ'Gܰc '$ȶp:eQ v-;N lEl%l/Ƀ@hyѧ6{$B%˗ŶR:1smccs$OO˻Z2n__F '%Bch폫aBiXVL}Q9 c20ĹrѾq4P,e֝v"6k>iʝr:wțW{HE:aDޟZ,7Pmm,CC**T0YZ*Ba@dd]BH#;BK4G?#9nXk!;:Sى GV,usB1 zh4ۧ' C"3]j#/d H"@.u!vW?(_D9;8]6CGv;D] AR~ Ni=jJ h8J~HWΰ_ZK>=·+Al8"t8Gs𙍶M-PI9@)^B.i)0pѧ<59ɶ-qOma)/ -@_Lʱ*?ezkˋyߚu/z-l6Pˉ"E^U%tnT{-yx^*sӕ;80q,-_td=orO~~ c;"7`5Tn/F2/*; |IZىoS>W ,T(B+4Cu oSg~fe0WQ޺ >a8L2zMO#>M♅6j-_ܼ1{{NX; Egf|V`nhlǀ2(Fas C+?wudN jͱ0+~LјXbY({q9i_ղ}9)XtkЯkio:47g-^(q#>?dm|䢍gLm79S E@_oAOY\b!H#}??Mk5q 1EЊ(qn^rO ɲ$.gVЍm^SDPKiY꯬L&ҧ?h{h͋) aCȆNqdD7+7P9*q: M,iYhqm'^/7 EH EMWDg"wNi%+G(\{ h =qXm,~ϡiG5gyC7PJGa|8]a]:No:`Ђ`lM 7>u7^ۯ͋Z\j$P?TCb^gv<} \ZݿG;"|tK/[P!Uix$SG:~)0ft.Te"3)|lZˇy9_vJCAz+pM_c4oh S=T=c' Zn~>+v:zuzlhh^ڜFvH章әZjCE3̢0V$sv_CyvB#( o<^t$-%OHfd.cNщ#0H#4n].d6VpaBGrG.6tJ ;/.ڲ"s%EҞ3-NW,שNſ|zoY7[d.yWVF5y޼|/^I>{wo;2{8E]/ipZ@,/_Oo?̭|槂iټZ^[">^ P#993|{}*Z3@oy]Y>'y}rC*ߋo8cY5mC)B{~v|> ,Rs8%g+?<H<_)L?<6dk\SsF5Nkog>t 8.GmA ^4`&&s/.ГHQ[C'7`Z*35yto4c0?< u)jG'#6|]zVK k5'߽0)}SLD^-gK& ߙociwAdԢ)jBYٰLC(ZdXVzX1<C˖SG)LT8<Q |~c>:EZc$J/EH~-gcj0itc]__ZO|ii/{;%r{A}YOoh"Y}F[^i,:b5UǞgC?+zʩ%tY | >52-BPR S7ʴ crMo"M>o|yOR=\'w+ES#m^wBRe|,Y;9 *16E/2ZhsU/?lQ;:Dt8Σ=:m l&mP tJVo Umx|2vhdRg\1r_r_pϓ ͝e; ^6-kƥyt0w׭S>ݶDM O ۳`('\%S n:tCry5_.F54!nb^l\YfKh4_5khlQ0b3Q0. %?Q4lTRj~8SɚOBxE(.ѮӔ: B"vr8[@L ߣ>sU%@ jEɉ*㲿NuLT%g.M|Nvt Ӊp֐F`Yvt:(stߓAY& \Wrd Zs WV 2l%l%o^~??ymq1sV%U" TVux&Yι"Fs"={~e =U|Xdu1=\0P"U9s fSOSσ_ٺ|j{ ?@,f)pdio~{l~. rmv\䘋.>Zkgku"?}1oe"Yh41]uN)sZ:zEhke 9pVSW$휃J'=C3SA2O"A.\wU͜UGkwd ک?gY)YzQ; s6ӎ4p IDATXVlco/i%m|\ˮ4SiA*NiQ/!m-NnSl>w*uk\^ JtӸcpͪլj8ȷEp/6[+VQ'[@h Kr A&o!ns`FiCh\R1`_ԟ39\΀;.D0RTJqJl(r,NgP9jA&>r*e K(jVN<{Eu-3܁߹Mav"W R>8dFEU(-%20bR5\l$x9^3P_T08D8\; exR|sPN eMӱDpMgChvSm; I0nSœ8MJ76!oRwetij;"$bekQ)pY>d+=ZU~PĚ0N;x+xƎ0k5>>cgev"z'qGYr̗7~Ɖ.H;97 M~Iű<4G@$KZP:)12av|^ϫPߧ~Yir%K8+ׇ $]]v|n'Y yKiDׁCEH+oڴ6p8-N>^EXC`k%x6>cvK NgÑZ[|oڷ=$;/ 9*O. r߈>giC4(~VF;\Ρs*==xcػ$Z-yo>xU(l/,ėsB g!r# Y+ݱ5 s>ubH0XSh>W^k&RPjv=H^!EƗ^!7p0_ғB6r0ʒw8Gtۿ1QrXSLY佽vF::h.& 9tzxr:16Sα#,)Urh~NiPk;G'ޡ GNiz$y4Rc߻|Y\B-KҙjIH>w:S/o^q9E=^,tvgp@~,?9\v4wV6Ph q^Y/pl]g6$<9dmSgMo\U4@rPzbδZQ0pILO)'0ϓK+D 4vN#F]9Z\2hw5!m) Qos֓<*hKkd\CE ,B;͜6)~EQ 8mV.p ;Q K˺'6"8X3pc38@;˱X& WZl@%=x:P8ۈ[֋=cccs(vijf\;۠J%Ew?2:t:BQ") ʉ;P.F;+|*U@NpFVckP=2MhNTQ7EN*UEΊiN.'3NUzɅ}h%R}Yj6 Hhu#r\1>xNGkgp)M{;tвN9윺sM۠N8+5߷,ebq gبkAםо|u*pH|ukB6Uso1/)sHC0y24-edRYW*5K!~S==? |ГDֻ C[czFϺyYg-糷~XI~N/B54hC=phdK !4dps:A_zh/cUcy g@ |DOֿGV>RlB7d)uhhg}(ӿcu-e( ?c^942f`ft ǯ EXP-3KBl'( *d)8\@amR=ET. ^M EYE l hj&>43߼豽D/g:K-VЙp@nG]hTis6#Ma$ߧw:̩wRoe5TҸ8&FhBkC.,@wA ws!rlC9=l+Q^6ŇCoh۴LK{c[b\{$hu0$;l:@NƂiD6 5)Վ;'чy(>#Tb _9v AVcmV$#_?O('Z) \y{L0.R e.uL~nS4%=No]IU(" UEm3>Њ9 {lEZe :nqugZU>zaE(7F4ɠ׶GNbk^$hPU(æ|0ջwa!Zꨰ/S7Cg,Z)lƞeG})68vŧvBV9xӿ4Ü^=AtyoE7auZ&< kjG)T@|c6#cu\Nq5_$ #c, o1N;@|K4#@ty'OL@b(vNO+=4xlpӃ|~\C I Dicj!& i-O@L'a.?K?EQZN|rphl4V~9qf$>]*ACg5f&{xdĿ}<]=CqM$H~ˆo}Igp޼ 6BE{U.7Ɔ+.¼,c$:(|r_)Bi_*h atcVl(QhV݌?! ?ۼbp6z.چ<ĭ;s=!)k*V2/ERIR ‰X`g7G*CU` _].!3rzXSJqS90.> w:I$}OchNȊ=|VW.i/k'x 5} o@+6ZF-{q;$&o9[ ;|+~]ndvELhi(IJo3; oUB6]-BO:NOrzӓ\V,cTvU,d}$t ;Y/MB$6&]`[ 'U0|/i4˗6P@_ ܷu&0`EYE!W]!5A·~L5 _hK+u1ŷ_#hͺDJS:#M6"a2Mh!Ri9!ZE`n ?]\o'oǽOTQ~dP':Gs((^!I9;6nsqt?ԅ0( Tjw^zAK MWweca6)D`ݶ4B[ݸEG4~B[Z$$ߧ D/䤜&~'=>Iɓ'Ǭ=![6w֮1x) }{0##h:Xtp!r] {j(yG6Q v*؅tefn+S:gV#9&5B]jDAm ~+Goh,,I**ҰoR0DXj eʀ2hwV!5DbD DOtw}Zݔ) =Ý :\}[/kt V㵈_=8ȼ69`,\wh- C`PyBhT6#c*/U喉% .߾ێ)Z"HQ'%56+|8qx4J;bPN._W*gㄒ“NA{ >$PiEqf0P?LZKfh*QL|3HƾUN]7ũ'4Dhl9CE g.[RBeXBD82ɊO~;6C^hٸl0xu @E^/_=W&sJluGM;wVa蔗NӪ۲vSӷ|v rh!b]?[Eiʅo{ Frqk(4s!|iV>`" Om!X}~)rҞ}B^hUZT}g/^6B5ZϊmI7)X_ʗ6oP&q|c: NMU D@0T|Y"%$ e`4TnXV5#.d:_`>^y Y+<a` 4@,VVD:VP@4DM۾&_7l }0~*]V+TiDa斻 ԰[] G?84 р5D¼z%ͷZ)ۢ2l(؎$?#*`#(#?~ Nm01[hz^G5Y%ZQE]p o \7BdMwV=)uC+U>'rv'wIw -k7蜏Ny9Q>': 4Ӑ#YRPVS)Bq[Ck<u| 2Go.td9Y_Vٛ)(+Kg[y_˂I#KV))åcp'$vOYq&!Ѣa?qλxc,)hp Eh#'ݯx߄ݝ w%e5սcDO F/nLUцaNHqCa^m2% o_8߼H_~EJf}!}L|3cFYUY=Ԝ%4TBDb>j0Z 3)^%5"Qf]@Y(^ U)l,yUh|[q@$V&B6<[LvN:hm]r z3#H"tӇt>|^~ ɣ/|ߴ󋷻khi`?C R) KE@Nx"t_]Dw TvᅝPPn4G}%t܍]h}pddɂ!Ve 5Bs$m-U K~L &eR+ko 4Ѧn%;ai +6Q0hC *+Q(XMO=q̊P֔damY.|I;QW^ϔh9JQ.*[׷]"PYՑWhc)rUP6۝EF'"f*ndžV 7? D*V}Z%jKeġ߼oŎ? F@*bB#ZSAJ\6^}ҵ]g \jCYf8c;0Q(̫,քb5TB(c+T㫤 ^Y8q41NNo~lj:Ӊ% GA EF аT?e|-L?"Fgh{1?] f4=q*@tͩ DDh"iHW@_ҿOTVIWoB#aV}b_cӒ*H#{hc&%[hdB*ڑ_ri,iv*㞉*37؇?v߇DY%N˜.~' -ZHɀ`H?JA;dߊ22}0dyR3'&ab/f/',w'm',$nH~(K#zoۣm7_X0:A7O:bx)&ʷY(R=-Kki[#h~^&[|zSSbOD9Hҽ:?®8w@04@4N^ϫ0јdd|}(c[ŬwK=-V @dcWi:ͯPd3}MCQV0LGpo&ztjY-B)9(!B nZa%B#kx;)]P$pC3SdUlL1E?ݺ༿npj֏#|'=1MNjT M+: *_#kJ8Seid`|VZH Ih|VXH:p:d8aUCDFhnG'*I8 Ȯ_B=Y!Z1Ʊv1oLh[Chi/Z女xPV`aMaiD;8QZ'z ;tr2uYqtW2\9{ e9@b"ڻ]Ӆń͓ڑcFVX{ʎ>u[w] b 9KKf̩(1$yaHHbى/E"WlZ+-W^GX\h5!q H,f^ܺS{} @s_w_'PcɅ  (Q'E. @yhY9itzl_1a+j ErZ1 Dmol\w*Qya_}=|y2Ћ[Y6M>v8}Jt3Yyh%{_#X5 sxf}vҴ6Qԩ1Nya'J<?WGĠvw*ԹWӷ SG V0!m`KOi's}꣊1DC( @yJTP.}ܧ!#!52)ӱkL$(@Hb334X`#(F"i(;MT_-'/6M۶DdYyǧMBr/ 8Ї*c˔gw-9qDĄ o{vXp#ed2vZ7j5>,-r|nNJk;0!P'A@o jThuFӐm[gjQL_Kq^AS%W-` iF4R݇jl Z@~/"'R/{ae ]x+sI˗ۉ0wNV^u'(V6ί?3uL ;H.bڑ(0߀SV6~:'B}Zk]3#BBMcK<ԮX2uPUDܞM>QeBA&!HOu&[8>0k:*i /NA(7 9r&N!C߬QHqqi>O h V#+{ ε̶3{{90>J1~$0;ch jWMY4כ=Z 0؈\KO㏳>7k eD7 ԇ)"zAj:1ai@ĨQᾲw{PG~`ȐcQ.R_g2pXgݒ>41+ ^B}39a^wxwN{PRGKjzm:H}*7lH"D5V}s٪z[qq?$U>onAu/cS|x*>~@JhKL7=MNUDPwm[\{'⾤\L~n$xw6']Bk% jp}0 $^n= ԁZY ehUDzF+p5Y>92}c'Oytk =;yhԶw{%Po=wxGiA~$8(}$@qGJ{Oʽo9>]h&B^7 , Cb99wOQ<~ tZƤu gB1=Q/dMLC5Ő1j~+C}S$jt{c! ]S/Τ.eBb;%uWW9u;z*䀺닲0agy>i" VN:ݽvfQ /kL4;}rVwv4Eq@01@Q5Sl0K&">K.n~։D'm' :}߯7%P%u@$!"{puRa21]ϻ);O7F!/,U'oݑy$|(0û*"8}I~exwؿ8u?ݝ>W>=HSXyq1{Ɓ4w/rv!*_Uf.Nu|wDJ>5?b?}I 85`ж/~zͮEם>oA+llSʒ5U֐LM (G%74 "b(؄g/Ѕ틐ũ|oz/ME|7Nr7eYB;ѣ1K83wf>FJC !"XތJRzjr?zVqx#N EN=z`/7}|I_TcXې(MJbiKըF,hcﰿtrW=zA@؟](J(p^37cqAԣG3zcU@`#^=:gѣ.Pgc NNѣ5=G=v,%l&7˭GcW"mȐUBAil?paC~;l/ ߎn򟇛ƭvj;n6n„s[U4mnFtO7qMy.NiGxv?YmPd}+UvuKԗ<»^ǔc~beњNC'~zyȫA~e~YΰCR0=s#du#o얌v+tR"̳"݌kգG=zA(ѣG=zzFQ=zѣ3zѣG=QԣG=zK|U21=z_oö^m#$湷U'ۭƮE=ҸYѣGlZeL{;v6n5zg=zѣ 5qѣGmخ$Jwfi`z3Zr 6|I(c YI-(6[UюF\/JM'LTc\cy0VLͽ`Qu^Z:U䇄8ϋl4H:?Œy<>Y }Aoas Bw?u: i}|LezF6ZQԖ~}`e;pG+YuTSE9t!tdqE)t5oC=nbW!Kd!qev{du4#QlSP@Zu$bKR8Ԡow ,5˳Stl]'GI˭1PL1 Ш6o@13_ѲL: 뿳{]5_YGt(a-&OZciqRo9tܷͤݢ%ՙdԙvFV`0D$:MH7n;N`Qc;IYKNd;meҲ#q: ߶1H1m?t)Yڷ`Ґ>꾐%"7},: 4(d%[IkyBgpkVBmcY73{]5wN;lt,~籕40Fu}c/ky6 TXXp/ppn:Nݷ:-ԛ_V /GYaCQƶtS^ۭPnNGܶKة#S6^9CA:-K;6i2.b#oӘmaI~:Wlʬ~hBFt~Lҝ>߯'2AG 7H2t-ӟw$\ -IY Nn 9r6$/b';K~mGn`Ϩg)n`e{sW}'8w-vu(.[(SO@SLϻiuϐ)M.Uc_r܁g &\w3+,Svϧb.8ǰrTO_≥@6]:Nil9,bo9em7 nlAɡ^ r_bP/KJܟ_Q{'qFVEQ<3f24KcvF+G:2CYPM?/̮E{NN豛t45;L^Zܳ<2S\}@&:ߝkC^y4k/#P˟!Ϝ.0$F02tN9=7SӦ9a^:,T+TxRZΦZ+UU*k jjh$hw?HւJCk:ˣ:T`/R.MٺҊ,1$&tb/oukM豏pg+ Q߬x+4on<`u$kYo\[aZㅊU1He"rsjui{lS,PYKo7DkQjsNy`CZ*B~J!Fs.C-P(ظZKZ.@B6T7*T߈h4p~9{;O:k̜Y/Xc a=A״ llԷ_5tr;JJأǞ%cDФ.n k,ײvG&̆3EIWc YB7iZ/ aSdkTחX_sfvY^cRؤX# m'>tś"Ld@H<@v{vB CL*EZ7md aoUn'R><$ mbLN4FZajDe~Pg[egՅ Hnn*z;E>vN;5Ȯ7㫙ٵz2'kɺ mldڪ k~ ydNVy~;!O7l<_\QOvtgq/da1a7D6"b-;u:a !褒KnCPY:M C4Ϳ];¾_hX^9Lfg؀z?X?(Op Z?dJߔ<6eHWGQ 7Ng ̮I Kfjz ߔ?Sԕ*W57#0w(dϧgRu}e~Ƌ왤aП64.8vo':1p6VjQ* 1psɎ`Q(L閐r2"2r vܦv1~pfc~f M7:_.Xf_,$'?)4t%q`wvkHvw?'nPp -Gb gf9\_F66;_>3"IA@5pRsR447 NX5f]aiYN̨K] LD\)Iҩ+ ,4qCpG! D>OϘFE,i2j2年S4|n>&2W| =#,čF>:~΢24U7jNV)P qew;acD(¥Eo}H 떁 5%4rׁ9i(`dfWC#y}~ w1o.RzHȷ~!<{UT}eFAh|qFJrR(r3ҸB􈡾VaT/U>Nu̔xy8A-sYPeWe>mCr'o;dc3=4r` xeH7_u awRG R7>stYO ȷΉ:/&3tc$^&kі6%)C6J.-f3H$}ˏA>1k HQ jŅd]P2t2l! ̝(w~Mi!yu~WOiT`y 5Hy}DiiCPBzD*k=5<\Nxk4'-ѭcG{}K1+O7h$)|Vt+3pR럭@ù]&j/.ש593iwA8JY GP>mF\Mw(\[o̼LZa}i/Q";;) MӀգ@:\ x췿W3c>N<3\hWwp7_1L~rVX4 4G5$Tkҝ8?^Cl:'jqR k҄oJ<ԁ2wvpd&LGS\&ɺF:']Y ‹\"5)gfF:#m8L "n #?- H,Mg'Sjj$Y!2INXӏGn[~K s)yT @'=ߝO|+83_ Bx01+đ2n FR5 3?q;nIfDv ]bFWVY+qx]o0e&?FN$% Y+d AUlܳe5膡QKWڟ극!3o.N1dنOO ̢˹su^,Q}m@mM_}rjWYWr:2Hу]UR@@[n! ,U,p~"UԺKIZ]yd"һܝM֕o>)}D kE_ⓟil'b0duJ_#TjYee${Hk}4IHKo&/R>}>mRSL;XR 2e6.v #4Hg{ tkR#w402D 4}/Yvu žh9:hEK :DP%5l:K!ZJϗS6,CrfE %V6U3sd=g%Z]Q]qfHX669v͍5^Z Lf&Jύ+v1Rԝ#9z GLLݥp D-ki0u :`zE%^_/\`m6φ͵5sSGOFyl@o_MUhw S!a8K6VXe^ZZ5^ciq1Sqn.O]r=fv#qe֬Ac|ʚYcX+fKv:,s.$/f"Wu&rC_#a5E/&xq(֜b[zGe&o"G9C!Hѓӓii鵋NmݤY3g/2sn7.shl!귻;ۀqۺ;C6SLQ."^MSޡ] M5/;@]e޸F_ZܼQ-򍙗9 __Fm%!( AVg;C}lPYbB)c1xV~< 嶼Te~5/7^ASw5!zb_z\\Z|2S'yש v͗T&5[eCwY/ ϟ{='V>vȘ-##NF뗭gdߑ}!ǧg;uZ8SEno3/=sH?S<&1f?T5x3稼q-7s|Go=?roGʈ@&]NZM:e&>: 8Htda6E?+ɑ ~N3} CXWyR4dsPΐ=`Ӕ7Kvμ~s9G~QC66) pOZ=vJ Z2w/L2򖖑y@I㧧FvV2Jc {6v?>&[U1rЉva$:hvh#_ɿA0f!zo:9ν wN[aH9mǸiݙF.<[dXrƽ0&k%KYϼp}Q:$)DouH;7O˯]OrhL09;R1>ξxUPpsx*YGBUD"OĿ;C[ +snA[l5wDyPF(R€'z#6ۙAZ)ѦȖoTXxy5h?vw yOUON'!4ZהJO*w&35Y./K3"ήZ#'Hfd@h({vM2.m8SwLS\.wHt@ӔIcU'VʫF:ެg}#&+<_Rx:M~!;13s|cY~[EV: 1o;ܲpy֬4g6tw = '* ""s|sf7GJ4!y>"Uor~vA+:m?0(4=g:#v0CI<`Fm(R 5MA$jΌ.Ztu S#` +\lkUCVXaQ$mSS'y F%Ԩtiʔ"&duM`C2ڜ|:%BSWEfzy=9z%%YPc͍5eZl} tc61ePWaft쮁ԝuZvy#'Sܟ38yPzcyy<M;ERfUNo%֓/<8{ĭɒ~*dEOH0 p?)-@jrG5bGR8M5:if2S<67ApkWCza36& ^ؒҴz$SI>O0'Nܫ7+^)2"/~7* Mq0Abys۠oۃ@zKKvRmr0b>zKhdjZOcKU_4Yo&:e(CO:s"U<4%QqYiKMg_+gFCNp?2z;zGNjS;c:|FHsG\Y;R)&,FQS&n8_DWWR[u L{3(|v[Ta(T};946C: ÄD#YX>LQ{HZYOG&+?Fy1㔓^z\9EAJ3sZ\QolRmYi obYS#ؑ]j>0ccrdNTٻt[ # T;EʫZ "k"g6X:i4KOggD@}jPq!d^XPa Kv׎;0%9"_Cɾxo[5glN\Xԝd7t\ovV$ M/0Iʿrtjt:T?O<Ʃ|'5Z<)DĒ@K`%.")'Ťy.m]n>RF')7$FRYZגW9?`7pFDR`?+EML8꾅Y+NF}"tCLx;Mry:ar^n# ("Vq\u["y L|P``I^ЊNWtö?/{(2 F.-pnMtkt&Uڣ54x1b{sguTʃcaTfMFfl=L] I3]gq}{gxu~aiD $Tbq P'ql5INqv(|%4+0t\Iz~'oQ H GS7o%Lba@7z0kܼŹՀTvIHTUMof-ƔttL?q^g#M-6yBqgM~FB] =Y 1]TyR/1 7yiW)<:VW[j`n+s4C4x}LOg P>2bO;0H^cН(PgU_5fγn>~{һNrW ׀bCr*?w~WHlΛ2݋QdN:6+[܎;Չʪ .qLoMnvTae'$-FZ mIɾ Ze jE^: YJվyPu/i0̎pQo?/S%cAb$hkn 70]/R:aⳲINwxZk_#cSo哟xH)4+T9sj%oݰ:DJC:Ltj\}4u8ֻʟVW AQ!Z&X)VqY3myKI]n-!,PGO/K^,#Ff_ǿLś"knXR’:îظg>SV`Z&<:<Λ:z0'O=̉)[Sq 0RIHțt[&7儮{ݓ6HrˆkWaygw"%"TOSʒtf^|qJ\7{O28(M՗t[5dH<^nd*}NRwSOaqa8qu^{NBKMK9SkWx<9xS HFIĒc{=XRjG*'/7x~:G$9ޜ}(C$uOH/069 i <8<L^7QqIz%S1@g# hJ_k$HXduqUu Ϯ>{۠y% I W6~8Rs9 ͜*KUJ>wmn:VJ֮5hlZueR3F"5¨E1[ϟIG7o噭W{7|(ՙ:|rLZo!Y]mpaix;+G׸P9Z@~+㖻v9Yʡ-s}Ik6i]#tm3y&)7::\tuZת/~xʞm·%o@pHos2_}q+5N?y;Sן)8z5V-ZgNzevPa9=c}-r9;G[dwfUe"ѥԨu&<@YlwE{mq` :)6o,XW$uO!~Enn0}[G>!J1(0xo`n*MVQ<%d4~-^LZ%˜}g OR/fy}o'JJm*{KֳY[S eqNӵO7+?ׄbQm4=LVIgDHB)4AdnKˠ9(;mze~x9~N4xyw0vh@w1NqxM_$'<97ˋgY\_'0Px[#xFs:s|vC&GFtzңyVfgiJIݲFfTS>p w0rkLJu)LuԽw,/}oSSG>.v7Qd3g^7=C;z;>w Y a1:!~V]Qթi\Hއ>ĭü9:Zлc,,E|'Mt42mv;~׉:U#_v*@3 ;M9ەM7Ĩժ:-}^lpyaד%&nNufٞ P J@DZ;ڑ2֘4gRhO T$6X\ѸjWB,0֧3tˈ]/53>c̘QMk IDAT3 Xwp`YtRSCԥ`Ҡ̓Sx.r8?{LJ*l| 0lw2)LM'G] ^0Ә9nzNmSշԈ{^ 5 bY(eb}cc/.FGK"_#L;xϏܕ|# 0O ZUItQ@'ʗ ѧ!(!nT4Voad[(KS[IG0q` Ob}z BoԖ|?ؾ:!9DȰȟ~;n.1Qj6" g/UŲjyrS/FnLMFN>_G=[F S::bdf̈]s_yr\*l); @?eN=EP$Hp2G"s'nwjMNERa&Jm?Y~$V, y:hqvczDhȈ*MGWT|+?OFvw5(K Kuz~'(E59sBG? ieT*Tf?c~RD 3f0%ҍњUҒ6inFQ }K÷c2)+NN7Te$KuzJRc!mi_ϯ^`Rט9lq!M-sd.@[q $je d*^i̓sK=uG:|K3}+ְ8"}c9:-nfbC"РT% ,\ZlHTSSw)sV : N:̡!ɇ0778ezxsbO4PszgƕA Cuq#B7nAnKZΙCЦ\cSjT벊=m1(#vw[_|.F+d Ja4Q׾4a7UTPjI?.)~~::yu̻ ˀB~S'y[NZ"h Y"?ޜ0Zt\+|Qgz2 ^5ml*?(/ K?vGph&/>>Ց"QQk.̶u4I` iE]U)=wPRkpHYϊ{=}uo<`ϧ(8U4oRgfWysk_uF ,\Z>ǔ/~JyZMџ]6N;IRӿmǿ;I./sԸނ~'."E(0+|O%㷫L wuYTڥSiduSxfKH)ІV $Q,!')`GPՊć9b #ކ 0ܗE`H4ǎPM-W+|/ra~'Fԯ|MBT#: @R~M?̩ӳE˴@-Fs!Rx ^K1赤8 5*HBBGT З"'=97@wa'Ĥ%'4J;Dt_ TW9 q1>CMGE:L 6? H"]Vo@5HSLS7*?ܧ>#Å6;=RreCèZX}2_O2T։2`F@3b/0Uǟ9GZQRD +vKʻn@vR݆]ɫOj{Ӥ7dh VW|%u Z: >.νLRp0_ʧSMAwi' ?&[IMO(~~ZO}Vr=fv#[Fԯq*Ub{ @Djyp 7(:THmv1oԫf-~ ;ɎvijpGڛ[w{BFܩ J"55IJK;@OVx~Y)wE\HAE֣:P wUWi S͌ȗFn 򀫑j;j7` MNHbpSFyS˷Vtqv ϕ:=0DzTO-9ks,WjLicSgFbNHUH@M[z209]1 M&pta葈ަ8?1V9R?aefr8O" 5bcri^ܬ}TPwO@dzT+VrQfR͒K7dt)ɳYd=|}4FCE87G+x~esc嫑]t[TeeA2R:l/Q)j}ʈ(0ToFCH! 8B5۪v,Uc\;M!!i (4 D̓PMq7(RsIuLn渳×K7;P)f8?{h=Y{߁./WjɍZM R`R@z7RlW(#St% N;1Z hOF]%s*4P4QCGLfPqzezFQ&o)"U*CsgTz:V PV15ξԽǝQ!K,8DeFuRIv$C({fݦ:a+1}yrƑvB ?tÏT6c;~p +j7Q惔HqI<(8x5`+y3߂AMI9V͑Y+IB]`f|GSf.^aТNk]RϺ 9ꋕ= \G6IJTGnԛi'8;z ~O?CoYy}YjUR[U#勩餩;946d}Ei@Q߬2JybTY=tUWPܑN3nRyn:Ϭ1vuM+vmWӍPdPwMA u- ]oHuǟL4 SeJ;zchOmdʽoHA*yv$:oB&+%u!Ǖ#HczHD&߹ٸ?V6ZIcw_~"0~fK+˳l* hys\[|?̓&H\вQiDi:,<Ob6o6(4`9pM$|#"g$op el9#=W&]8uPAӮz#|bGFJ^_]jAUO%:8,ԘC)!Hg$y*zzO?}΁o{ף?P,ۑ5ȋ1V5*sT.̱y,jXXo2PLr9>} 3^@cwyѷԢ<ۂ5:iUw/ͱ']?YG+uQS%U㵫jr1\ q~ W̩Im /']WՙJKWؼiT!q)Ԣڑrs?:ZP k|n ?hEio닜T8axop~a;6y w04?k uҮ1;f-rB'~:uT7lFҎŦ6S]:TA$ 9{F=dO4 ${4(4ġI!dp" 422{mk2kqlYX홬c!Z%~l<Zܶ99"b>|5Gu~/6%~߭[uשsO:pSDwWFe(~ fwWH6_^aPFO!@c R\Rjćq̆ 4o+jSN65la&sýu٩sj#/(6-`f#_*Uxἵ?9:Fe*{z(K Wkڭ5F{'A9Hz:IEH$.W})_F51t%hPfK״O!\hg ϴŜċZYp2N JD($mwm.m(D{y;)sLr8Ӡf>Ǫm铩 Z/ O=G(+< DW-G#̀&sb 8q0cGXjI "F1^Bxb@X-lj.TQCg>`&6e&PcԨukʌ )h +۾~1+Ӷ2WR'b%H$Wߙ%Z\͔Zxs$|o8u6/~:UKG76Ғ$ Ec:Ppjvl96ホkovxmp >%5 |yz.&zE3'.SJGmn}3Ss@;p?~gp폹gsLLX};cGKД1?5w4fJT@;^cL o_UޥN]Jip}LU%yGE.@~m% <=2W#km,Q́ ַkfLZ>nD2Hcݦ͝EݣfQ7 '(&V^)Ѻ r E/y˓zd,g]w;v +HŜo|yNf n Ei~P?MS~Ԭڦ1{FMٵ@qhP)hO`83HFcg/{Gp:U^45Idzrj=|Iy7w̶n+o-}=|lߤvWkʗRHXTՇdQ7<=so31=˙I{p`@r:@whHr C;s^ok5 PN0Ef-P:UBPC226amΌcbjkl[= :FJbHGxh^Bum:[@tE2siA  l\J PTf\g,}akRML|yNfpS(J~neb w ODmK5mo# $h E~1SMHian ?N;p7_vqi2E#G[xXH ba^)N6FT[$B{|4=A򛂨ge GeM fXcӼ>=Ǚ3g)ͨmBWho^SdxF  {91^ D:F^ VQgqWuʬq;Ah?fT;|LYjPR6 ܝ";mcdǍ:-IJ Ejħ̤4xH BzIt{#zq³?-G;q6+< m'Bh$ERz+u|۸꟝bUl\7 IDATB@3o47ڡJ&gVh|cLap&~{x4lu:Q VJWB0Z؉h„ur [/>t 0WIAX4h<7OW [BGȚ}lGs(l;q44?M(ql{u U(J g$z 35 $D5*H97t4)1:IIx6L@cnY&Ԏjvhq_m쇃\_cNT*Wv\ 0!!MI 1zZrxKL8=0cE5)B`[A/@5GYHȂc38٫O߆~#Ԛ@ ۓBIB+U"ʡ"gv2-(n{K' n^h$fd>ѠJPmo% ƕUTG|muclފNZ?4W|ڭ4MY]XV6ێ\ >iU o$:}N;i<_$~|2hLg H`46H/cqPa BcڡaEͻ:LM[RQq~*cp;q }]lZJB= B5d5bvpl\cn7FVY!k}ɂO;ʑ~=E3퓏[ŋr5 B}^8BJ `J`2VuM#sJuhN jsc48Sv0ӏ_6c!\5nˡ4 GwlZh_I^cwleP?_|7 PCJm Im]:~Ϳx@JBP{g6qUPc2_N?~Zи}/k;ev`aaB;ҁr*CAd ؊ tz˨_uv78Z@Nyꉟni@0r Rfbj^Vt$Cw!kV ȮgubLw ԷgV!3Fߺc3+N34v, \Z4B9e}ݻo'/#T2BvMRf4X̹j"AG,]PPH[;sfln>l YE[e"H enbH2f:ӛ5Jf x醻dS݈hk@Z\#1 |w7j1Tf`D?yeY.* [mVxEdFx6${ u\=@" AڒlɢF/?۠0[yY^zY:C;;[c9VK DNszQߎbk{߿X: m1~3?@9ӶRB˼RJҠ_ԟPcp'zIwr1AZ/$ SnF).^ͶpanN=oi{YM^5uJY5Y4= mLA.:NH(aQ`Ap$oL2tVqoj+łظ |ipFOK#ܻ:ث 4kGv#-V/Ci1ݾ3i_1=N5R? Z ǏFk)^s3\,U9v拏~>njT;~{͏WC C o#mxYϝ[͍И5TN/5"&nG`9i_Fn @O{H)cW:f^Mx%7ϼ}^b kit,P 9\Zb3`<-,2ц ґ8m$4]-s^i Q jBc"qbph7z58sF{W.![66j/.b71vmB ĺP/TRn#K/LW\d, N?CIxul[81ɀ6WNMgi}|Z D/9CYFܵ?d *9сjIp8 ׃'Y ߗ@ x4e@j$񝂝d] g/W<{3k91+} oÀKJ5$H S@b*OU>oC~ W$:y K?@KI鶽yY~N-qvO?ƼLӖ V>f8Lݬv84VK-NQB-t.pG_][`Ks!R EZɄg <+ y bf QmiZT~ 5N70Xejāzз?o8G^_ PijApF:ӰS͢ ]ilcf܋?1}iὬ%z6Z͒sNSvC5u|A< ߦ)QGE@_a4oFNq s]l]n4rKcd~rRxԬ55__k?:)AHpB }a.a:4C*.8ck^W$iFh$aƻLMg}NGAjkrl}3|!!뮊`ʭ#,D3OjmEHm-@q굘Y9ۆgt,WHT+%t`icᏆ1)MIi #QlTL+d hz1xaSʼk*@7G,sszΩHMXy"K}/!mPrLOO^Ƀ<]/,Liz*&g9s\c)mce&B-YTݛ&Zd,XPdTfT!oĨмoN@z|caM3@s'ʱホvXCmsIEJ*5)qya԰WsH MBulZ:."Uxv^Ve&r(ΫzhӭO#u FiGI8)HUOh ~K$5b!7&뷙#/ 5/e,HB5!))OTͼBQ#f#mkwIIŽuUP\<g޾brJQnonuW;w0s/bbG?I@ 83Xd4F!FZ͂}̖C6U0ւBh*#闎fzj;n ?~l|:pUׅzo!~MNR(:vwƐiw5v~ yÄ C ,_c` ^\oOR!{rՀ:ǏN`pS_Skr|Y_ .O6oGR~NYFvqɩ^rCWcuܱ|>OTZ~PZvQGjm{ i *-DUˍj.^`S3lŰ)uȯ޹BVl-,햧-[^y6v޻ Pz#t4ӿ2w6YnWz U/(["L"N}^T 99.Q bM z1•:uw2Ϟ䕗_cp&>v~ S,tS{D(;=/rH"J]}mBmBoNpT^'\s~Sܭ=jt ᚐ6R\ 9@1, 6 "PbfVz fS7S/sz-RC)Jf1[ՠl!WiݦA;7/(]ؿueCBvo0Ӕ-L߬.L5ԹpaOId5}V|([Pc/ ]؃-t8-ݢ< ŷS: $ju tyƖܵvݍaȄAcx3q;P4FƧ9rS3=5<ħ? *.2 1WWE`bOL601Xc}#r]U*;F9&޾L] [͉/,/h#f[-kc'_ꤋAܠGь'uXe|{\KYqdܘ~J;G\F]EBi\X?cYCs$@rmC73GL)X`+Vp6;Kfsx1|9 F5&WW5oC5KCx|cq1ᴿJ }4v wpucϿ-e>г<ɞʛ-h'v+u$0UE'C;gua@O1N̖\r\/Wk\醙2Q943ݜ㹿s5V\|!np3g);[C شnÖz+̅|a)?=G@]dxWg;aAwH@1 8Y/F1͛BQ QFNcld Ie /^WL}٪/6eٽs C2P266tCpGLoFʑ1naЀkmI@iVߨMl05tv@-DLBh>hʬ*ߵiW3V4NJ`Q#˼;iDn Q!NV?̻6q).4C4mFsM W\%#;\Uk@ v8ߵ>=j߅@]KS!kF^Bp~۱wedpq/F1E(ZVBoC{8o/}9eI4ζM n`0J<؏2Y7 uTWk 62 Cix Kɯ-ƚjz;kK9KOw{7~bMk.֔/-}P@";v ~Nj?>J:^@*(_VZ[81v7'2 w{ՍZc~5AI8p'{-lhz :kb7}$i7ZR/^X 0/p '^(Mįo@c)[ii_,(Q :O% 5yd49JJ/QE京 }kՑ]# W(Hp[?r඿w3RH^Sf[ 5e;*[p`m@W)qMLv w =E'|5߸7,_݁_g5wNPp~03WguٔS{r2P_;C[kEϖ[׶g7*~p "kԻPF^_Ωbaw}pz`V]j k֔/%S?=^_Evl?+\ԄנV'nYPB\~NQ~*[W3ߗD4BOR胰 @CJQ"!&F?mbA@gG1=Xcq=V EkN&y7qV\]wYiW0W \^V9AY(4u)g~辥H(({+GXO ig &Eh)wAKX4<FxZ|bLC|bhY]( v/*cB3aէ#H^ Rqvixe5pn` nmԨTNɤgsۂQ;ɟLJGwf^_v;f"#ppg~z:w0LOS{Av Kg9>&\i0vw{)8 g/Tq$hGGPäϊ $Y %v̥){Hسm[ص{:X5J8l]Gg\h< | D4O|N`%hrJXr=U\6/~Dz EIHN_PqO 7{G__>6ZZ3흴O'pj߿ Yp:o+K=w1T\WB2tr~BEh&!g#slĨ LΒϯf6X 4f)D pt7 UJgl$ӹeii;0.^BH50*_1:4;tb|;z;yK JkS n2t>s#3Bjkh-co1$~6K di4Zip㙼ǿ<-_ Ξ c4h9sn޳VôCHZzN pe"nFJ*Qz3 -ޮ=,qsۿD֪3`N|:ٔO >> oGOy^EnbxCnvl .Q%JPZ>m[tO/۸{dt3LLP@q<|Cܳ #`Z@Z@ĸXǻ; IDATYA;4\o>vHL\_n~~WQ oh@M":2JF&8>&'ϖ!WQ#)LB>$z{q`bT_0a )@H!W. ~"&:/i1Vk@k$ED }bfs˰AOwtԱ0ji(O vA]MLssDtwٲYMХʜ?d4 i~_,Y0}j^~|7h.xi.;"vy1NÎnsKA\4c4U'6H޸wwbI ѡ`[4dY/e%Ux|ǚqgR];>bʍ8fmS3o_~z0=(7%Ca_?)ͦƧ3U[B}zmbVs!iӐF/- Ɋ)[VP8p'$ Z&QNz|QNot hJL}`Fqݛ0Pzl'vbuʕ7^B)uUьJ^ncf"#A8~,t ֤'zzsG=R_qfS3c+÷ػV \#RSH&?bS}Hu_|S R"tw! ꨺v<ƭ{+SVB( f' fa2.`YN|jAKDջB *AO\>҅ q|U&6duJ Ks[>Oq'mwBtonV3q;JfAm*탱C| H3U'bz9hvl'r-[ru NUJChο˓$y}-r%\b?P팇C ;?= nZ_/Yei'nZ,i|Ҭ BQVfw,:Yi ;4h>&\[_W0M Z%BC͂S+AO>ߜcjv===|y^{cY}D|,:׍< i4a~Y.>ԥ}$Tὑsw/1$7qGv/|./)B[v"DW& h_Y_zCUY`!iӰPzYcBY> BMD?F.XQOMI.;ۗMGRisG;Joluѿ_~c%&.sO/ -WSzᯛPmQGy8XJ,P$X/+~'ye Mn:읁M ó}pU4ŋm s]-31u^ga.+0VwU>q⭆v[,IHbCX,%+qH Yc%FlkyCi&.o=JX iZԚM ⳬ.[AX|f+K8OոZ H+b`.>Y(Z.,-tA/.><4n ' <ڐ4b#լg;7&xKr]컟cXcTm6 Kilk~蔖Zp߸/ t$ KzkUv5 tD3?[~( ~ d?} ~~f,d},4tfLHTAsXf3t᧝|5RŴc|pxI2]کBК~!5]tѧ>M7ۉ Eu7M ڙaeiE?FTEBBc1Qm'J=>0:6Ź3\S~k{r_g ;׆ !yjN~\: qQ&Z$h9 _)Nk-X`ד !iolܐBQW89t2iiLNe F(7_NSƬp_G(d7Ki#nSO3=5c&yVt)5n5|z*sɋ'9Xφ;X=1 3$v op:M6tqvk~?1 _GR-ը]V{ZrIǀ;̽{>7ū&/Uu6{Cwh=_Kh$BMSSY?mk] Dxcs Z+ab2O iQE?W煖o4z~NEZM->U(b#-̇ƉaIμ[O.D .t ŎHgkCh6iuRc`&](ohP풝Z4QSO2{[瘘{y}۷@?~#NOqj]+<+b>i$m*{+^RpU1 g]vQ")ugK\T)X7WFaEϭW)a-tN߹,]WRyw {훭dH~ /& H7/o?eX&Ѵm~ZC#_=}C|CTJS octK 秉t}C"n&*S "z=[G&8>&쾋xvQʅB O,n Ͼ+eϗCafe#& 856ɩ:=F P’SrxqPZ ?$J65*l))5* l\˶M|>% *G`L]J2^mB 9| zs|fKĖ^\,g޾‘S3[78 jhnAk+T= 5^+읁D7~vyЏ>t_!-|(.Όc%}~u<[l?>>8v4~Ŝ{ r~G>sstyN,0 KUF'<띂F,u9:~lgZaayKò E,7&=Q-_܌`M_w[ /K0qf׮a~{xJΝ'~ʞ'K5˗.aw/1w m=H"{wz/ңc^$| $|5 (ȯGx'6w2F؅}Ћ֢rUbnsODx Gi-Q)s` Z+#Gs^8|~56bŜ.uZiq̀H:s.Bꇳg~BY͞|#F06mV$T^iePP kts|31ē<>v~+pJ9(I5bYawfdd#VhX a6 mPl"_6"&X_L(K#?ޮx!̂by)IUqwv,6oUmgFM>vT4ہAqePWsa, @8o\I 7 `pMPc'FP[]/xY&NsfibdRc[~ HlIΌbBsvgp0ϔE'YeQ-9Eѻt4$g'.*чAe@meGv:|q~ZJi3:;A;ο׿/'?xAXVMJ(艦&7L ';l FVFopяLw:]سJ--ۨ5Dn'n݁Ps nȂ$B|,kl* ʥjhv塸_@]VvF (߄ ]F[oGLiq5Pi+!<5HE:2|߃wp }}< bS3=}=[bk}f a7WU" Ҵ]Bf]yafle5DϐLȘq(|?@pp^IFǦ#h>s:@HLB[Fv?DTӻ0Y4\fjtKp85B<>>`\Wj떱U >_λiKeOIfLk@]_ϿU~|i4UXB;H3>Y-LX*jQ9v{ͧ?K]YȪS[S1脞Ocit>$/s%̖ cgg Xʈ\|ij9W-PtsWT*jtF4WZ#U,wN o\ e}jgHoRՓ3<߮O P84!:B'š~ N.)FE'mNʮ-E>q*L E5ÿm\ @\ޱj0e'PL+Ǡ߅6}[љ/|\OXT҃ #_YJh'r3G9BڣC;⏿NFC8ؗ+': =98oX 5μ]br A.ב_S[̽]9 з][nUdzO&PT-5voz< :?iG=̧?F}RFX<5KJRB+@`.Mj/m _HZnDAh#c|m=ry%PzbÇc ;ŧ?a~ރE}ۛBQ+T\6WfFחjqvƧiPJ@H\_2er"7_H}lޤBֻ3OV:& O;<.7  <A=LIQcc!/I"1vRnr!MSṖjg_KO;aOi~L<]麐TjwHI `ʁLp[Nx;kEr<1b;M©4?i&+]|7_ZdbgiB٧J&h- ݬr#iig'~8w|Qu Rz4 iyf!+NhaMH$fu Qi TYf҄ r ݦǙ~ h`@a EGykm9B7=?xC|k  BiH [g^G)Q ppߞ@ekLR Rim5$t뻄H]y| Fa!Ϟ >iG(NV]dm,g6|Rc[YXv&nT6{e\ijs1ϲhNfNuVRt%M!ޞ1 %$40CCZmD8ZHمcdަ”QX~F|Z |~^޷?49Ů/.Ra4#<ws'g'P*8o"> In\:c[yH%Nw.ԋ}_ڷxꇏs4͞`"i ţ>OgvP-Fg, ƍPF50pf Qo=}OLRj; wJ9tZH ;~6rJ+~T㫄|nOߠvLÔ+F[L`d4rW~yJF`cON IDAT }a!OƵkGx{:T< 0dVG^{cWHi?H( `+ׇL@RS -J3FBs5"\=2On&-idխ_,o|P`;kI3N4 ZJ{pXA< N!y o`2Pb[G.kY.S<~r"-uw~Z~ԂMx%w}rn"~oStop'T$s"0{FXkgJSة@ຽ0ٹc\!s^&sɾo fXaB;i 䣝8g3T'"!ݟpi|B{Pm>|봭;*nˁtY6+|aC8io:@GcG%@a 洝/44’Y$ЯC`@?}6Ёαip ?( ۈ5>=|sʱۣKcخ,t|뜅Ŧw„Ȇ|KB{|>_?]rapl7>Fk[ IM77!@bϳqڎkLL`VV2Vh~Њr/q4ڙ9.L0B. H'_'6FTϥh@-Z]HFpbt 15yorm蹵>@aa蟁D5%X:CSdaۦ.^[JEF^X|:-roqF ighw`/n>Yi8)o)ß"DiTVezN7|IߴQ"@/&RuE#[d#u1b:t=G VrlM[8z{Uz]` `CĤk92|b$P..`'o2Y_=w"_( /sWR+wWpq.GO(LC!g-wKA]QIA˷􊛗tP,^E9됟JLo~D1?zygϻB`g;i8svXPN3lvOcp{W7oixW( ȍbn'WLǒ64gO@_Wqn=[аg(&"ku7G=;H2'`b'Ű AϢ: `xE0w0gaP +-ܷbM.A9\y\j aakk nD8a%GG,mf:l&NC|˙B.Ta J+9^޼wdUtm2A:iطE!nk$2J>ZRgg_i`W7m iڦQ3lSn9,"?g|xfWXN7NpKOfAa(bf¶wщܕtw'ĖJDqOx{*Ư|uHJw9('<I9:yZik;ah9#rNνg8?)(K[8֫tY˯.Sfi0Tr Gq ;s{859~p=GF݂SLfF2^ݟZ@!a gö{1't4.G͍ki8Ps1_=ǡukx)& Y$+4窴K,>͏^C&ɇ 8<A>*Zb""ᱟl6S]&Ϝ&g`~uMSGl+TZgk6eog&w+Q1d-4?}?xZyfYCGq gy<{r˼h*'+fJL~!uF1{rᗒt˧P@5AQrt|Q0-G≆f~GhmJeMY (0mmܸCZ:̆εܬ!L妘W޼[ 8h`σr5Cܵ x~m O1ŠXyj-<@(M]F&qa(-ΎvjF1۳@;zEv.?Yp^@IJGpOG)ۯ\1ixv'_/&ee?3>C3~<7g/@3>’30` i'_w\uJ4w:d2;?=g+b*dip&u7Dfߡ/+9n\^sFWkT>+D&p,N=^:|g&c8M$;_H__:̩b{wp 8_u;O)r6~ =d_Ry=2uwoqty :JXΒ֜n*{IYV L;~Y)^qqWW#^<bx7{T]n%W~dt>*/ܸ{=-BB[Cm畷ͼgϽ݄[o^uoT!Zun$oau9B&cܗka˺(HEwmNT)q2^GꫨoH5"pb[ͷɖ-b+PAeƌEF"V+rRo_bۧ+bLZ2ʐ Il2{~3]3>w8[U\cK1&*ga/^bWWrv^~{Y(+ӉggjǡT^ K1xTU̮KOu$ǁǨGixUaIju,[p[ ɏwFb/VZݴ$lmm7>GX:~^˭ՒM32w\>̨Qd!N]FnAaM,CO_ &ΧE4 zuiYc/U xܑN¯.W^W,/[t:8݋'~Jm%[-ӭ6F}},FHaՊƢLP p>cӖ:Gy|JJᆌӁLok(=ګ,'ntuvS:%3EgHc@=EIma"SA&=-`?5E.PPWaX3~(I$Ȏ%dݷR=OP-jmaY !"HAYc|Q~7gpLѝrpgfLcVWL@F@$,V4 Xb93I}ϑӜ|V!5k9X??yQqT/=Aqw2NJ{]w@p~}/(@T7O΋ w_|"/fU(Rx}CQd?c= CoB+UeN9 귘\^O Tov3S|-=Y&ppfs7x/IAٺ+Tf6&W 0Ńq!(`*hI4&J3մVQyA!9qfGλ$`0-9r3}0NjE# :Zj["N;e.+=}~aD wC:yoО˵7bCZ;BLGR' tʹ{*{wl6|ϣ$R%bzEyaLMQ@%o{S^Ӈ;FU]5LcMAb'd:]x)s#gh-$=;N(}c&vwO^k7.?f( 뻕Jჵqq]wTX(bwp*]$.4iqN fU~ w: ^67N$:ļ-|pLM%$ikNΏCTq#'oh"Sg貟_u=Ifx;.T5dFBG~ˠ4tm]8]pI£˰PC(ibZ.*nlgc(Nu뗊8~Ug  3kɋxi|iZٷ:e8GL"wC[f*tuǿ4ah{80o+ꨋBܸh[#~LߘsHn#95=d[Dv0м2zLE!chx8{w&ʭ[ٺXcѴZww5L'ETƳ{ݳ^$nRL21L$S~"1L*a$ĒKf۸7na5iL>KG(ۆΑ\`I4@+ גɎsh+C_&2!`6¿4΄>S|+ W4sc۷I%C RI2I$>ܷb"z N24QJLxk4ߩ/\ԭNMy.u^m:۩H^ns($.ze[=yؽS7ʏ_.SRRʲtȇqW9SHѭݣ)_ +ᯐfI4$v ` N9<~VbBE8(b8`-ݡr.mL`]&4 &bXA+ g_YpDfX`9E(LhhFΧh5nnb3}]t*62~N 9cy2wU IDATW]O$\piXP5C>ML|n5FAMKJuX/Օ+.$⪈?رXd/ eX8VG!k !b'iTo$3@wkcѽ&1WJayIK,ƣ5;^)qq*G~)-!?N,7SQPc+a)G`7?)`Z atw(u4 μqS |+c:'JS@0VTӚ`×A U r;eܧJ9:q/A SE[rhn,]˷w6wR%֙.Dbɛ8rk[7P$B-L hx4J8pOr:SR|~74W>k/z=Hdݵ$0j>j}qQ(4[PR))P#lI$Yn Ξ-c0I u|F#g5WKb"w&;NapykՊF!Q0;1F^k_zQtDd VMh:]mb( h:Ȱ znK8}VaH^-JWAV:i'[J>n5!uN,[cX+&mĦ;[bGӴ=5Bg׋ge%CGK-꤫#?ϕߕԳRuw]ÏKh9*X/7^O}G?wwrNy W|:& sլA,V_VtSNoJ]rsSmDŽyZ3O8v ipE!g=_^҆̔Ҝ~fIzGsMbhn[ EE-hEӁzOƫkr2^ud&4 ,1ǽbnrѕ?zv$Oe}r0.rik)?lDEԧgIV3̠iUR,o&rl>ްD!&g/4v XSTU:W]O*cǎ禵(>10t'>~2K"gC[ f* wӂv7xU]YE@K;/(GT'cf|y)Va 7oBA7)[4q+~A*/_`ubzNuux)fT`ƛ}ƓWӒ[Ti Բc{|tBJە R9(N%]̰Q0a+7*)'V; !!ډʹ>/js:)' Չw8@o?,Lt0j\Q-'ޗ=Y\/GXdml26X1~~[bxX1sIyrKGyu Es),$5ބ^?+f ""1GA^֞dz6\g7w}BrjiH833O ė;Q*BLt]#GuRÊ{mLG= ݋gOv\˕6qbN3 opV@iH@;u@])6}ݝO 7ѭ7;$tk>p>koaXw]cӖbd& c8?=|7`i-d]|- !CD.C~^nlk;f:f &92,65Tk8ǫ8彮_i { #<M%T.鐔^xvN5'$+U/RtV};L/rQf7* 7$o+^n?N\ qe14ªILG~HXU{!6whizjӼVP;o0M"6n1FA-4~<@vL-C78P6Uci[T1([ ׷֌R&9@7T&y]n._f(R~1iU-`9C0v{b90<|9t]'{ ygh~q ~uor܇TZNy(|ۇr| U"Ж0My}@0'Q/o^Gq? r,u?@b 糼u"uIK5P5hHPmXaޱ6+!&yəz`];k>nݲkɩցۊ"&xk7P^a9A YzC>TD+7f גB|x=#"fXurA:[W]Txf̒/9TzUd05 Ey'/M2Ay}?V:V_scmb ~&ϙ|c0!@4\i_}BG14_H]u]]R_OwO/z-+{Ejk/2!4)Mo 7)_pGFA#OLb]+fq nنW&C5!`~1߼"#ȏ F/9Rpe-uj6(o!4[Hrӂq~=نMXk&g[:nw}O3OAp`-SFkT^uFRpf73lCB65u SƯTT|7.H7&ՄE!0^3+ W{*~1PPS!1!h涆&Rd謴B t0hKz9$A8 d9׾DROv^]#p(1j$cFժsd ],ΠivI/o^f LôZLi%U-2EGPk0Ír9:]P[|u8>nMIKaX(Ecm<m(TBXђH[;{u_.C4UZN d,m R_ 0PUӞ] Ϣ˛K J0:L 7Ԝmj1O#arf&ׇ kalLfƴ*k4ߤr_X^.dyckD*~V`ߢKXir"yXy(h\7fU1/+ӟ5NU;_|.<fIr^ī~q 3^L0M)'yAL&vS>Ϥ: 2I 8ԦB:;Ώ[oAu:_I:aI*_: SV19q^FJ3᳐Ym"X`( &:V(Fm\ d:pŒhdZXRgwVLa^_~/)WC7U1uMgRMj96di<j`z*ÁGhm-\^釨2)eO4/UUr0jCLbhj8xp%+<&DT*ݷQC]Koٺp$͌E9WQڗ.&ڸĞi*8ZMuiP g0xbWXF1^ܳGXH6\*56(0^{؝[>p="UC5M0.iY\>ɨ*"0|[$ríZk>aᄋum\=(_f(:;sωq3)ݧx.n{7n&g}q;ixLFΩ۲whd߾Cd`h ذ_~DVk8. 4/gA!i6nH'Fim`@/(AA 8|l*I8纶)S, ~˫L#+[V>nQhhdxO3 wqg7UzB54W/77Ad]6P0 Oks&'#wh iL 'S<k^r{a=*]NN5/};r.aKdF׷mUn~13{J3ݘ)|-٬xiV<ηZ+'/uİfz:"}c;;1Ɓ~zjvKgayd^p5ģQzDfҙ:1}'Y\Ol|V6]vtn> b|ppL 7e#YuX&c~L_C ds3kz gCBϑh]DG9hO@ c2ra$GqTm $g;R&G?w'A+l]n LL&Pg/˚iHqD?=?)|p}4D`J*(i3>@c(&c*BWGLɳ=*PăJO :n_/z E_7䂄㤘./0*K[1񪡫֟~=[b$oA9q̚#sf88ՠqkuD"lmq8s9m׳cIvZg*gG{y{]v-7G̨Q胷mǯ8bmܥjzU KU{aȞ)<{U+ն\^{]ЬsêR k?AY7nnzJ/EfiGyĒ: 7`L//^9Uv~ VXR1ĺ`$Ww([֫@/n˗54 ŤDC>[LPA5Aν "Sma+_%`qU@AN'EM891Hm7_o*5n%cp npN=09oSn+x/?FM}fLHg{ {_T%kݝd{1[̟ܻsRO5zy eо^MRs̨Q$0{7A5GFSA6teӵ⢢`o 3x>00AWXCN)sF̳=~mlDAju.GjiILDP۪o̓'y\x-;e~E:$9Uc %Q@9/sx1Fh,]LnThWm@W"M2XG 0Ua[NSO%+Ӈ;ݗj49,Ano1;HY/7H !V]3$nr i[u/?%Xon&jjX<ǩSOyQ12~Nb`hHtp,W.Bï(U_Fr_Lf(!+غ'50^8e=P:°?G]b5䅗]L_HxS'[X74U_zʥ\=ATjdA$d+\:O/]5E۹Kx]l\[QsL {M}<ºV㋬Mfb"nZđطtvYw]7Rعiɑ3b$)zY\σw^`{D35HaL_y8 "E܊Q2Fa>%3>BZ bR~D8zypy9i㋆ܱ!}7l(87aTo-_GW:a(?AqxW9:+nJ-D^[ 4J"}*Pf0pǯ|qq(bWR74gc?WeɋmnypCGzXmm3CT^C6\Hc e/0dw-gG[bb5c?1H{߳0*#m\qfƍɕ!ˉW%[/_b#JNE\OWXJ_`d\qiKb)/y//|w4kr_xPnZr)T众/ i쉯hhޫAš*5 D!??-_϶';#pz>IqD?Zd>1EE[DдZ c4GDa{8˯ɉ_')-[31ӜMfPBt!nrMJ "!% wxMϊz~߼=*Ugƍk٪#Y Sl4&f݈z !CA MLLv]N.wv yhsT{n/ mnDW[ >yNA4]!S/%'<9t*ׇ8)zqjb~ jGXP?_yb͉4b'6&jj8:v2;Ɏ0zvQKXȺ/mh?_}';% 'A$as :+Zukx;7GXX%~q]gY{(ObI)y=Q47|Plݾӵ0:;0p?Gu_Rrdr0Nʉߥ@< +K ˩/^p=T&nP${$q.\ėV rC<5Ze f]eNrd^0! mih x4Z0iql2)H卓bBw,&pN-:)yKkX0JW:Lc [:kG֐ط/Lc0ux4Ji1:8ƋoAor}Ģy}_uK9q6tvXe(Pu@t2 " .+J8)>LElaL6kAFʊ =8w݋rd_>{yy'tCa_of}L' כ_|f ?7?hv=h&ǻ^/[ 8&;pC6t̓'ɘ0f~},{X>k:xv{{ׯaGXپk{!$"޻e;3^(oʖ+yG'Ȱ- pw/wVn]3rNV]EsBox[mDIpCҬ^1.Q4d׶523a6XJj[a8x~|}9ŮߎRO}\LDd;.ηPwŷ٠)J]R⇗~?~^:;^֫1Z5sA^ASLWqֳb~%Ad(Uw\i S\RMSLC9( a?0ATLEX˱F~ Fִ5jt@rd oLLP?_`JOr՜iAޟA$yC,m\5mMEd"Y@@5j^C\{cv0FF@lfS?xȑU+xZ\FO&$M<~3Y]ݟ:חj[L)O/*ץ@A/й YlJj WC^LP?oP0“V=1]5*Jڔ] _Ax~ҵ_I1B۵8A(&6j-0sr˭܁:o-|@ j9aҵb^^(V. EQ١~.CAs Wϙ?Z!Ɛ5Z#ȷ:i)(z1T4[s,0L km4Q헃`G\n(Lc*3n& lGkYݶDӊo_DG5qy ;R?_'oٴf%0G%=v%5%A1Ca7^C` Ҫ0ZN+|+ iH`燪x[uKU{K[Lw8+OyXq+d m;~{^^x-ɬ(jBIs"JjxHd>p mOL$}Gmm8r?tТퟑHdafbnK&EW 7jt%A髙(0`#tuNC*c`8Mc@*ƐRU?.! 2`|_Z=}];x8xS lag(p,(}q~2bJRfvd /?~q*˭ dڝkXu8ړ)S%EP[7xutv3kNgbb&Ѝi/w:}Z350%yJd JX%䫃FG%G ǎ7I k>~dۉǎ8lQ \{;fc#RBpt҆#QUPTcM Lͨ?VwOOO<[굾իz=M&%VHxM@!~.y^x05IIly?ZVذ~4b) 81˨$1:=fr ]ASN.?l^N>}'B)U.LJ U,F4d=qSM60]#A]YbaP2qΣlQ=[^z/HC^=-('S]IoȘ S`ߡh)ZkciA%o.#M| YQRY$6d%N6Ҕ8ϢL hL !4pXQ!̇Zr9Il41ݴxzbճCKI|(q` eBu20eOfS3=%JtT *PŹHRIZjI ]|IO= H0 IxqӍ_)™/i)2\'i2Pq$h{bX(L\7c=;ߋV Cv\@ t47 :9 RcCOZ=D,b٢06~def3 JX5q^Vnʠ|31WV?};7>~"͉pՉ3S$ S2{D晕33j햛DAFGjkY8pu5#2#Q!.7Qs9rPBAdT&B L%boP#K_*21j]r #> dZOr0BLW ùaΟP]}lc9}uWk1W]Cs$ʧe3V^]'ǭMQ%f;JMhS_=#*ǫ)6<2(ֲWÒl9/w;N?&n~rmvK'd9,X=qeiEmm$e|ks弧?ȸgQ, =]J ьr 8 ˯\rsߩI1)-L!q>q3uxhx{K3}nxDR2z^.QS] Tc ,;v_w|?uM@qd3NoʧA,FRy-+cNY2r[PH)C_2{-V^ϝvԼeŒGޢ` @"bhDaq7>d]GU ;Y.&"OȒGusC+PS SEL ~^ɗ=tl7'402 YrxNe>iBZJv3&Lca ,eǾ# opSMkó1s{bQC#H~c6S]byB$cbx1/  R Cn { 2 "YCm􍕕YiaQd-kb4}eLŪFiCps|2Fy3ך:W~%L5/4!# *8r]_27,=,NqCp0Q _ιKc1GCPdž-{Gu~L3>osokeLBahhq8lX±O.8/qI76pRS]i,&1zO6;w}Ȋ4- X[ݼgߟ >D 0p6}`JbH1YLQT//w/_ (vtr4N/!|o4Xq)&3|G!rW"׉W/TPl:a 'R^Ј"GFNeJu+:97sk-` & 8J$ceeZK_6K\:z]5z Hl?+ `N=D^R*I,$+np? כbI1r5I O4\ j$W.2d2gbsoY~&oܘD 40썾3}s*Xv|?Oo.,cH2Ɓ7f}~N~]K} zLC*ΐb7x8de:d~P캚K{굷9JhA=Ma6ޱ\Dz̔#q"g/Ũ;ZJg0y-UIj4a1ܽ;zNwX 95tǐ2Ktz齸*`2[-d._}j<䒛%?x(DO ˿IjZM1%tt$5צq1|xM|w|h!K^AQ۰ Y`W/ωE~@ @jkidu ?O[oG'ô]WmFC_ 2zr;gFX+ym!V޲[tM E.q12 DF$`\olohqÜ(6YMB'Bs);N&I˗lF+㽼 "*"烞J?06>,%xB/ M,D< ^s|.ZXь2ws"lL IDATq~"x$'S(s_0JU \ηv]5 " 4r0{̿M8`^k+ii 1-\{;[2>Eה3`GLj|,14ށ>~bs Tίj1p%1\)K1&)ݨ:|,ic10р]rgQ*Sg4B"PJb$N4:B$DSUdJm`-D@_=-VܰPJ9]QIrԖ|c}@GJ3+ ƹo ?:,psN&}~(J7a/\ar+%)YVյE!qC2ذ )C7ܲ6_JwLvnТb3zr ϳH(Q8?@W$nږFJB_Q:~Oyu|\J%AP-^ _P d~T) .cGh/<2< 5@]eEOH6'83̮bYD F eWR?vMZ*yN_H^d6SWIq1"U5RZ׷Nv GO}^ύ E::6urc'Ј·} ?P]+'!:ښ>+;llp ~Mu%cHHR1yܝRfLQd,셫:8 k!2KC؇D-qnzqdy?}kʙ&^_+ŤKbd棐8~K/"Xy&bf!յƇ>g.Gb܀"zΙJor 7YךZzs*"&EY!\W)+xˇS4E2뾗{(ɇW8|7º:!b%OtuԄ8}I~g Ⱥ:0 u<*R;;vN(E IbVPt?bkFH~:j]tUl:KI^ }.-rˆ8؍{ jC~k=OkLE< zzdEIOf_adsx- Jz [wЈXǷ~~yY 23o2=E(Gd/5_J_JWvu1F,Ӊ5~m40:^'>@G I&_4*mAPpKU55Y>sr)0+/_r;e9;pctuoG9't/>$34^=9&vϜ ~ /~M~VzzoG`wold5Ɛ)[#z !# 0/R˾oF2FQ+ȳ[ńgOD'' 2D_oDD|(>˛NbtLzMg;u`<ۙ)%- .tP|H;(qi(-$QNy8g!'2H1cֺ 2q=<o|M"Iܲpecc,y<Ј¦jh\X͎ǬIҹttR66F_ig/3(W#P ..n}ˎ=- i1-[vÙN4ЙY)#IᏭ=FvgN6ʏ㞾bp2~yyXy<ӎey)۫ Kkڏ9䛙ne͎3N;vMNW,i "OzN|c٣cuuTw|g/er&Ncne^n4L^[669]6Ҭ_{8;i˴8i"Ԁ|eeɉx㹺F%˽T䒟^RėBdmc)ӯ* $MQ!?k7O$r>z(FaΉJlqqT+WnnPb)oYF5~ -.xI]e{iޖ7P*6}3QvMZ2豩d58 / 52a17Sܼg|NV޲j=ȿmmcNޚ-<6J]fީYr1FD( 18b5ɗɅM-WtχSs_h8?u>oV 1i #:ћo4Nҍ4א%#MH+媔xiǩ[x77;o*ɕ@B1FȨT>d2J.D}|;&oê9>Q: ,w`(m2&JIO՟}٢0kH&8u=e 6Uj IXoNe\Z7AwPL4:ˋ{lK19=,6F4 %sr-mdt$U8}dыtvcxWב=Cn ]`TW2rç`ӟҹxwNBJZf3iܨ)Sߞ*c0. )HGZFP pt5cl׻YնqiE.63)}~1ALYd2eqK\Ln=7cҏ=0^z+I{$v)S^kI}˼~ٔ&/O<Q Ï$n~ʄ~yxnn2ްBd7ļ!\M$cʈ88 >rz͓y&)h'MKX޴0E plm7L ϣegr^|̈́B~66r9Fpu*Q$K(ԟB)>S1ԖxZh؉v:+ ;0Y1~N(*=G4y[<ēAlXB0 u|Z9DnYRx *(J2C>rQx>zLD. yNrsѭ*8nx sjW_Z`}998n+xj12$\!qQtb[XK:4֮z9a`0tt<WzYqs˅B3sR^ׁnν $YfxAQTQK$]AkaĘR>D4F7Mht4E,jX`fL*(S O6Y[^zO8rSHɸR!SOUTS8=EהSf9:Ɏ}G} ?OoMwZK?sh=VH.2Q(tvUv{*Ft;d˿/?^y 2ݽq)+{18*7/=Ć1x"HUMҽS3n{Hvo=z]Cu<w1L(bBzǫC\d:U:߭.t/CNT(q\GOΏ=`?T66Ye L'0<{}hѼвF2GKDKs- y[]lYJ0,YJ#gX7 3sRh& B ROVHVfly < k絢I=g&x{C9}6-G }}U4&mc1Kf $CIqz{B,oywH_}ׇ4]˛d5S^2Ӂ>D;,ʹXJ:XL{{=TF/8O]t*܊c^^dA7ѾZYT[ؕNE{;%RX_ͩ!X*w.ִezRH[j4uT6n00\p,0011x+9st|am*\Ǿjut}buOmB~u,+3RRYN計yAQ=A&a$ӭ"YQޘ1de v^Z.CYMder)*%Z*CM2JϧQ"$jY*?eSLdi9ccMG3 qi;_RtYv߼R:^5vWNEeu4 I)yg.M 77'žK3(iB`HM>?ўO_SN "~xI1W[޸bQi9D2U8x(D1?7@EMpH}Ϡ/S][IZiU>0Ks$IlBr4^/2*bM],ʥ7L9?KX~â cYIT2vž^QGU"J\V3?rޓ({lMOO_[O'zA#$FrmL%h+p~ zIϙ&o*=CBa\Eo*n]"0r%i0lcyxUnr-4J)L[x\:ByqK_/ /뀪hmΚCh W!˄WySvEu[ѿ)Nq? -'S||.eɌ#.@]UTWuq@Y1QT5E<'2ؙ:X>P5Օ/1Әal?!}H$S_ӫsfOa[iY8ں*f4 sWJ}4KlXْ-24ܞt6p+_|(DΟxX};vW"x쾍d6|5rM 3-29&%$H@ m!J\C ̣[x&-~5Ra(N@b4Qy&x<2JM**9>9IHpz>v-"6vV?O.7[uO氖I1d.O&efϒRŪk沬]oBz][A~m^!q{=DOαgv4UFQ7}nY7l|o?{ Y2?8YjYX+ .C8ܽ0Kh@'y``/B(VR{)DH|LDDCSl9Lej/hAE78Tv} MCJFjmj$;[δS @?p2jzCSJ Zw?x$[7Uzzu~G*)ŶKuPKPVEC37?%/ni!v)Ɏ~˨X2__[Mhj UUYǟWtl{y!:ښuI }}bw.G8|x?eǾ#T=WnXݷhTU_Ć-J_o'ئMyB$Ά-6N@wF:V~}OlOUl@tv&uGdJ)= 70p  tдH KQ^6veĚ]=?DbG=u 2=fn /eJu4% ! yD^ˉ0=V yӵow.o+HZ9rv^sz95V@k:fT^ۍqIK v|p=GDC~7Y^oCyOQ_[I(T䛃™ 2F-sW]It:[ (|u8s:Apon\ڶXƊ.1  4t@g n!AM􄐙XO_):VbJkQIbM2uQcQ1[Vpyܼ#@|U//+I$[<@䋎$Aэj~=ve 6o%|Ƒa<YQش&^s[LauX#ƦDy.}s.R!mdf9M Np`Z2VO9몉'X\;ss&3FQ~^8hzY~솎Xv^M:ixYzۇu^8]^L=˛)*u4b4*kXadf?sgVH|SHg'ns#_O5+p<F.]YuӟlG|55W|x/N}x"Ord_l&&qnxߚ,ot֤ΫgygU.yf^tk&b/NҒU~tCI 2`\3^0dJA!݊_3Ewvn 2xV6[ $n,IHum%ȕ ~vG^N4X$On&zP8 <13L"7ѓ)7w[ۖ"!Y(g=' tqm9{O0ufQ[ֶ,^f<{'+mhbxЏnM0ls2Ο%A*mhvZWnB—:YC{wcrO$в6}rxh䪜s1<O+WeU$)xW=Ǩv$vklq7Fay:p&~)#WvE~ץ@&ZmLdauӿ뇚%_3F쒞^~0hfuӏF][̏"ǩ/}" j?v`\+xm7tggE" $H~=#Q*tҋU4{ oh~4Iy 2kHк-'}IlOo={6kM7|1$V:SQPn?1dUbMsu'i^"6216_6s[cL,2 ` K3 RI/urRJc4JcWײ0r5RW^Y* *s ɱe'_°cKNhlw_>}&!!gPǍ$z>>5ĺ ~!/2!)ceX,:.DoCw;&)29*: ؁"qÀӑ}]͏9 @ `XbMxm߷=GzZ 1?W^y@(,I1rı+O+3}N7Nw7o(D6xoNp08}W^y'77[;1(*~)_rYNVci,X0YHE??{3k6Oi7蘥&}/]7k]w)$H{k+XנKsDb(2^Ek 調 +oYʋ/c ƍ1]L{ec(6MSWHm 7Ga#ah\I7ٺVc"5Gg5`ٽQʒwJ.s5`\-X y_KVYIl)0 ۻz&8q>=TW#ǥ 'aTE\Ȁqϭ|&~b7E+-mM<.~F}})BG+-=n .cSЀ6Wz9BhAԸ%uXka1d 2u0cg{7Xϖ;-m1,gqcN(*K\8>`ur#).eޏ_0)aYJfЅ鶛~1lim-Y~FoK[ש ^͍ÏE!(Q;?( p(M_'vV,o$I}sDc-T,.eOlN%C#'ʫkF PU.Yn >Js5qA.u2=EV ^ZPo;KWwKȣ.Dydf 6!' '=<ɛ~2˿4 'l5FϛQ?UPձL!${2?r\J~[+^! M(J b5xҖ/^RjyȊ|Oѹլ}j30n컣u+?a#{wc:a=`%{`+IYL}þCG=L\>ܻ{/"r<1_pXiIXqX??d(r ME5.nfavgIkYyRvṇDŗvqgÜrxF(r`&bhYiTφqEPQ2Ziml$(Fk7]]ȳ1_nv#.n0?!:B$~M k3`I#Ł&m&fZrÉ3]&V:U5S{nϵuFvW|0%I.?o>?M^(%^_0 F,A-HxѐPtSDFlm'CYIX;F<7PGg *S-3B6X!?4ƕk8w=%c+7NWE"(GRRcg2Dc8sݪlxt5h|80cR0oE`E\}f'LPz4dT@t~d.YGuQaW,fR0^J ŗvqI>ƅռ!kҖj@_jbE%cb=O#C3'?Lܾξ[7cc2tzҥJbu֍b{{hj^"bC柸A A,B$0~j9s+F >zTpވz҉(IEA  }$0/HT4ӑ@NGTg-+NnYNyؽq1mFW%1ކ) k3h4 ]? @<;wMr*$ĸ}A7. ŊýP:8w߉5'[yF22&Jj7ʀkH5i`JRץ^j5r{N%{sox0ʻ[t'}̴<τX*@ONWw~,7x0ң&I:S8.nlP,NSO8?˲&$ S#+ K !IfWJq 3M{WYRwG7}_\N};_(>(4e7Ft0+8ltTi I/}Wh} zxyggF>[/v_f>Hw@Q!?$NyFyߤABLǨ+܀t}j#kpYJ ?hD%Ǒ'cQʀ,ᑇw9iz5+S^e rrNMʝ.=E1?G~4Ã=ssS gdX>iN3(Ţ?)헏B+7y]c ]'VbruG[8Y2 "7ǁ9D#j<7–Od!FXV!nQ{Cv3Tt{*;^bmbh PEES]V7Z.՟kׇZ֮>}mjjbe~umI, X R0Fh bbJ$^0Ǚ;w}IΜs<|99hva:$ ?:('+WůZ_$0&e67W_c@WaD%|r 7:F={N͂@C߯e3+xO+L$t8@b:1њ\fdL{@e w?lf\E\}3 IDATBDUtvp;w Xv|GSgb̧KgY^Gj+tKܑ-t0RxN/EJK׮0f˲|QV$ ~a9a.[YKybZ~WxRxłXI5bH+O8}l(FB2# 6D{'F'Ɯ\˿1@C\YNFo>H)n+鞍t:ъ=l&V+gHއC3K@p[o }f[VА5$eaf^I!U:$T~j<9êX- 'NMS_im;},ò ( D]ndIӰ#JO?G܏0x4BmHD 66o'~th>c" -6$gTMŒ5qz1%0ZnֺԻ#gNx=y :zRV^m]Yb#{NR8 ',{)Nh)މ&8mpKc_TR%@ BB#F "rǛtuWE6?? ʽbުz$:x?Z/E5IHqz{x⹗fOmy;£߹HS;I$ MOGc m`XuVp9$wm%7]u/S0-sJ,ʘDWS$ҏT\3xiv)A l!0=@[g?W|a*gPxv!+jda P& >q"|Kh+ӊK\F=JߺV~\6y3A>*~}c<D[i~Sl$z16#G "*L4!T"ORxv!kɟ>vc7{o$wNXgPHLukֽ.GV2qS^pr.5@e $JZB?WQ|C!v4{뚺 旂>䄘%H c>Ox N9Y׀"pˍD# kŇLeΧҙe'@"QPҰ|9? /ǯXCOO01*B"{c3>XԲ2-]ܹbVFc.;w%Q^#S34 4LrX*0GU sH|YMdW'@5̭P\qxD A7ԀCi.ET{0w(pM5ճYYYld, *cOc۫mP>>1ǮV]@v- 3olzYE” I:ٹG]yV(`eU(HK:elnOA, D#CUCUEO?B_gkֽH1*goHY0zuOǁ&Ok.6G?w-$;~}4~.X)rpw ָJum,pzH4%c_#9S٤מ0FGSd{el`p4:x ?bB.;N,8Վ7wg44$}6.D/9JցĎe^b~(6oabuLP4̙D@Rw??{1E 2뷶{Q8*Ş RDQ|ujJΚ|? D`0$:۱z+h Pt S~NZZ#Y9[(QrpB?ί?H> @>\P_KeI%qgqknӣ;7"G[XwFI7ŗ~CqҫIDJ{_疳|R@}h -KXEn2oYˮe݇)+ȕ Vf}5hܑ#HVvs6Yug*DҒAzRŽ>EֲJnCttӰl1A@qԱ~k sv~-Nn>F5i_N( oI hjneGYyid20܋qUc&ǒ'd74W:>£߹E@dY6d);cGR"{uά(h8-{h\ Ź/#"X0/' X0 ‚i~V?U8;fo`,qGq4,Xg½u ùc**Vp9uInV]^HHe:UAon]IyԉwU:߫=gn8- VƂl M!\K-¦.#N.d~V?>zhne4d/+efUpキki`@)Z( 9(vaLa$$.lO;Mͭ4,_] I~UO@eIcfSJ,͉c~TIԭ1T Tv4E;}arj O=-N͓257~zyAs1 ctfP-h$nW%X9*t'e`N6~ ~&HE2q"KKKe3+/O'Ġ/>#qjjfh8mմm'~};ux~C쥈9صIn=WXҕjRӘPeVtKzM;v N3]_UWHĞ#0a4=^FoJ$mJj,IX/:ocfZ"/=©aL6U5m|au57]W?^8W }WqfO%{5$*E'GaEwK%h4ʭ5 `Al͛y((g{C=~_?,ffͬLO:SCI0e7Ze x-lBǞL]Фv7.Z\qɥ xmצȬ21_$l_LѶy6pHyMyDA1sዣU)_4ϗ0V1r|4 /[lD_ ?AMleVQ*]M㵫OM-p$Ύ%v In |B|'!ֽͥXqU#)7!1: ^J_U5 EC@ E"PBR>Z,$ǘ16ٱ)TV-`37]WGӶ(UI|_ZV3E ƽ ͭ"a@lh.o&@b iq^͏ӈ73#X8CϽ-h!hXPEcJ V 2+{Ye"qjnu9% H@~FS2Ek!k]:8WWE2#y-]nnټ͎=N{ĕ1# !6 T6#X3#4Oٞ8.F>X">Eܱe_;(/%nGIStDtK8uc< RKHnJ6aZmejRi()4OLQ=["YE4AOL,!nŊ-qe290lݲT wY1%_z(ȮU"_Cpm2 ̞Q`*1ZEhFc]_4~p׍llζW^g۫mDQ]8tu"q睟% f><<Ɨ5/a=DIWUNl ̮=̮,6[;L306lv\@m?HAԙ8&itz6|vh E$=h4 k20O'HMIE@<cwMi|4՗oܼ1*`'U)v6NadO\5\rJIF Eo+As#-MHp {GK:o4j+~ <,e[*l쁒0{цX|`pY}4,GiV G`0\:kݻ__I0] RS]2?ȲjpZv9%xp?=]2տ3bJ7n^'fM75p+7]W(l޼]Z?W5l7ŝә)_.y88JXE%hUe~)VSwN`Q J u@IhϮLÒzqBb͆l&F{e4l9U7{L1M[78PcFfkF˔АʒJf5VrO]mfT!Xuϊ[87\9AcRزsُ, >9ـUdy쾨d%,O0P?d)39eh'Sgp;|_!dY!{| D}i9F4*y 8!Led (TO2cDim4D+?$ll9,6^}5{7p4*kQY;ֽT 6eWK+.j Mi(H(~bû[戞#3~$ crg&-eDw Uhy~4䧏ޫƻDXCǮNƟejc#pr+.زsO7(^)UO~A.I@F6ǀ Sm:2u##kzOz=-avzKh_0 ?VY7& {I>6M%dgp*cDe,pKo3p|}9 mi+Dh+@ƪD%Jn圊 twBPFQ6o >"i$8Bbg$JAH5LE@㵗ؐM\Ʉ kShLK( ݬ,oDʟ!#ɛAyׄSMԱb?WSԃѡ$%''h$hH(,4aCFg?~a5Ł/$w'hXst G-zsEwIG$}E4Ƽhj_~^ںㄦۘ>zׯc\O>MCMZNRحN5dɏ3FaOqS4+_9Yl1CI`[4io{O(uƔ4kDth-2fR;V{hZi<لss7&F#_ٔ- ȏ[V8/ٯY4۴ݯ{$JK5"C\V|f4qO,>$n'{:FЦB{|Nro?rzVnL/*թĬ6t%4OÒz|6s=S V܄PgX#Uځ8-_F>pbfC9wMu9vY5&7w+V},ÚlwG6{N6ٖdMxWqtem ,~z6s/3NsY}U/E. }f%°t| O)ܴV+9VbzEp8qӦ Ye/ͬݸ`4 !O DQ‘8o}RʄS:'M$! 1 $M2/cCRxH% IDATm$9c 7㹥eĊ'7'D*nq܉\Xd_Zټy;qJʫKD\2LKkY}唕U~%ܼ& ,+E. "=Cɻ ݺT֥-.!+In+$x BObq1Kc  %\Sq*#;x4DȩUD6~X86"Xq ;ͬo`8Fs[ rsnެьiO?έٿS;DA3G9O{°2+swTMgO\#+/eC6X` Dc!?{h( ;z])l{m/7+Va.ITw>p;1\w 44$f:>%/r\XGnG6IǑt\4]~`%tM VY3>P$|J"KKٹ8OVNRt*aQ4>CQ4 4[HhƷS \d%4ÙcO.}iQXS#Mvb#⣠yW;C"̬m\J7o2 T4MZ2[g zH~af"oG9%giC1O>hk×/ UK4^nIJf%us82C G‡YzrbHl޼~*gm3=LӎfBN6{&LSq#Wտ{Xn MK-$>u*Az:x<$.4UBӹt2 j bdŨl_C+dt>AǗ3@Om-DPYP R7ٲsW7.O @:JWw&+.b]W #N)]E§rg$a agͬ`VsXqpnh()o2ژOUW|慮֫:D@{hASk;3K̉[T$drk+@J]$TI-Sc?и81ݫ@e{LGbQɳ9p⳺;SIebEdpDid Gty7=]XjC)  hhC -y|fn~9+W,GV 0'l: UN:37_r""Wl(ARuO1x(A?qV5{2E tK: LA$>lj|7\NcRAQ$~ĨHͬG߹4P/wiɛkU |>+M;? VwônPPRЌP*>vѲs}(:͋q#~3 v9$mׁ>J薕`l+pWN⏿>&T)J0'! !:7:q[4[f~ID79 /(23Sߊ=lz $!9`Y*JhX0( (/: ^fw۵aTV/e"⦍<~3!\ji<ȳ C4VartӝK"lj{4ڏBߕ$ۘXX|?q zT'd="4P$oi L#4OMu9 E.#Ž2.klhtb?RsjW%N/˄{i;Ýw|4ͶŲ+I%[uHBWt&Sevy7۷D3i,Y\ǎ-tkt( j;t+W4P_]G0ϺiY65'˩xu6V]W.=_3ˊغe9,4}颱1%Kj&\k2JS'B\.Nd .t oaR $) EHU31GP~¯'7ANx HFBWĉ^fڸ8Z2 UQ4_KXq']W,tfXZJ&7pJqguKN1+"w‡N2 D?'ZG4w]cfx IfVILDɳ 'd@  .V\mY@(2E ̿bns6%LʜՑ$sI@SJMO^c!7bjtyxGW5Dh!{|PXbXxߠ* rlana;'Jx@cEˈ YE|YE\w@2bMIn^CU?K%lFnܙԋI|\@u.N5N-SUD` - NEuiȪD\k-{ <!O[XXDB/ 4 ؖ䙣b$ '=ĒT1H|4RfLj)V}m\IpPtCGɀu Qc $PƠ'Xv0]\u(~NYʼ ƌU7D+ԼOXt >ҹ4VS|)BAp#} e;`PA!ci`tt"<gYCC"dv\#T.R쥠h_yW;q~KG$cTg/&!֠idaxҹw9Hm@z-65qфdB\߳ (~Q/Al0vDcپm'eLkhgEP;Mvan9\: ΕW .OB =]ШwlEvpqb!K(4#%;ƀzdZ K1";кwݲ#=b6r͇ݿ9Ŭ$Y:oG<26g|2S@Ĭm {~2ġSpvpkUycV3K!P у b!'{$l]tTۀƕ@m/7DQ5*س[-crणr07WDhRVVV'\{ HtB!:>H:+?P#b\$(ԗT$ l&$;{{ݟS US"< uǹsJ5y@p+x7~U"m{R.{`vcͺYË{Jl6F?*y `漎 '.cvOokeْj.qV+r陥o`hQ*=V^ _e3+<~3>|}FhH7\f8YKh%qVn\E3A;}׵, ?)J1dwb)'܉&/ʺj(HnJnȳ3m><Z͡*=E Ce4r= qu .{r|?+tX&KC&p;ۚX 6fSߔCkCk]H,I 71v8˜G#nwL0lw/ g Xq'_v抝%;*Qx ;ƮC}G3OÚVPYtz=- Y"c۞~mqO\i ªZ\u=~YAuvL{@tƒ`8pOe/ B[>ܭ$E1nxVfW"3r20-fnw{1JL 2o6L`dAV׊ng>c!an7qB%$(k]K>V% 13ZSe~0h\tͭp;}mD"#qc~r^q)Ƭ<2Omom> LQ(PxN Cebo+)ʏێa/L/8mJBNG͒O|{<*&tĪ|TX2-{+`)8YXtN #lٹM+9U@ƂӉt$ k>`Kh_lLk%`A_ Kտ?aoiwwi( %Y]Ӿ')7\q$7}p$b=P_K4\Cٗh3oh `f1ڿ3~3c,wMAv/*F> ʣ߼4"Q}oR8'2x @8oO1ˋ.gqmby@e>n |p4+Fj;MEU^&q Urqr[MBEvItxioI9lHR_b*1z)4mLypkֆ[\Vr(1'wg/A.1P吸ߨ$[*e*4čcMCmO*R(ҙB$ Wt(;s@iׁ&KHRRr;cV<36O=4;vpÕ^WC-mʿdky⹗zcBDcCHS햕_Ć.yMq@@Cw\gn{8,QKP@(oʠ%Z6?U. `mLNϝ7@pv?VIud~%M֝[v$VˍhҘjB1 غe7VW ࠈ߭ X:  @!rK!MGjGҦPXT{{{qd3C&-|Î^[3ޤ@)Cf*BØ!r pW h8κwk8tN"n0!59c}c8.}U Ɨ 5ʑbdG|̦zn96&vllқ?Y`۫mAN0O8m|cH;{q?Rߙ]Cv~ ӕ~"aX&LdhY_R޹03|Sc 撽iPM%Cq2@{VV줓l{NJO)nX |1# sfP:ʏoz&Fge{&?):P($ w"ӯA,pb?qqY"Sw;$tfYWΩYר-ɒHS#-d)NXX1ʒ\3=ֺN.\95\$G=]١8V'cpP}DƌӀammμEuάp Nw^8c|cSNcQ%IO>ÞO'n~V){NqӒ+Y6|J*CɳbƍW\`B7JG6 Nα~TfB" A R;L7v'Kz$jױu˞խk$2IOJ?ݝœ'ui}i_S~NuhXROPV0*}7Lq%7$  sfKL|mc:1'ç'ɟp[w H[ (ʀBxpۮEVMͪV6<[pxm ch/<|r}^%[rO1!ވ Cf raw?+EΆya!_ ԰ʿWPpzPyLBn,[v}"O.yrE JϻoGG sgf{eAl柠]&/1^|YwR G[Zɴj|VMw_{>Z9GyM#owm/n|%Wo_)P n- IDATq?֣)g򫸹4P _zD+LZN{s3| oY]q|TuS(H~]^l{w/aw5*O+u>qg6m4g?S%(;~yTFknW,cZ5E޷(D]]gxr[ ̿|$("pS?L7_W[2|6__kFR3vpHCg.J3OD4?W~ŧÓF7[/yxׯYޙ1Ug((3 9o>+}qg^y_ V7keim,8\kя鐱|ғGY!&wج+Dyrzz|mqy5ȐUJ9L).`ޘ. G_B9g>g}{Vx_ņ9yN3>Ü9TݥS8o3; tCޣ:E6#*pgd c]saX]iJRH>` L~ïVX;3dh'LJDO( Nieñ<.Zлl:Lw341̻oZ"{OTquӛ|Ҧ%Sߧoc3+j*>Ͼ== 2xA9qcw裆zc@|:EQygp{ M@f1zH_)Ɵ<<<<<S_4r6op5an-#~ b}?aG\9L b:^e;0?8R}63{QwN  md;>g sR'}PUߟǴ}dիϛg^mg6u$SNcw)8)c˹ye~/Bμԅx巿'q*FNѸCR-5?ESh>:wwFx@:W ߢ#|2(V>ۼro>5ݏ(/|+ Ϩ y] ;=3{GU|s,p]o QZ2yGKBq3{jB.}"vĬSlY/_ebO<ɿ-ǏeMd!/ѳH22*#;ey5^ K^:]%6ryWxxxxxxxLRz6L ^²"i +a*;wnE$XbD['&:ql[c''Nd'q6q|M6޸[DIA{-0E}ι;g^=:t ]I<˖k|n@ /5Ã#þ~UMTz y\3j_~ eo䥶X3X1Y-9HDD}cS$"""23f=E[))AIHDDDPR$"""()=Ff _MuOvn͟ʷ qwt""99=a9>!}%ʧNWڮl| Oƺq>J/kunfEǒqډ|aC|&G苕Lj*Km?9{3Lqq-^#Z%93Xp;71!8XɧƾJDD$L›?x bɥ*+F ]ōvNg4.Z3 ՜oaSofh=f/z|7_o>'/I|5Dἅ,wT6=Ә/{2H⏶WVA$""3hɹejBN,?͟\oc>.1,_H9ڋQiW#on]G"<8 };Iq$tzx1;Ŏ+bfN{.?&vn:[m3xla.?:~E~jߝX/W|~YNO62}-yVI-+S\kwD<pz:WgcdR9F_9_/EBs ϔTRgN~Ȥۧ=EdUmxhk=v܍mVxyrkYQ~WC탍_TUf7XnBNY/dēpKσl/Z7Ym~HGYgjw!(~sgv>ʼn,TOw=͗gc>wꑤ"ג㍃)56>\2|pC{^gRs0F,[{({|>mx7HO|=isXbM"RQ?L =|5)nn'#c%䥳G˶OýN]LWC%ufcR#=||Ń&jm1SDM㙘'mLJ3;xbM^~oj4 \RF,ov^vdLٔn^&?4e4k|<;tefE1} ':'voDj0l})#"kst$(Ǥ϶5EYm)^V=`(wfIQ؂lzw{cl<źݧB慨U:ڙ?Zn'UNOeC55pc8""Iff1 É @4X`chv0GښGb)"";uCǣFr&uk+Y;T&`aցauYk=2n?1yI7J'-K)Ίewi]c\p|eYJB&2y!ۧܯr#,H"oT&_*3/[o{q6؋7&5}NH&ÀtK8 ]<X>L_?v_%8Jw3x?5b|쭮gނul>_Ǜ rW)ZwSܮ0'E7KN['=u&2mquN.-؅|'<_EzDDD$Lf,)MX?,  9[x.[~m8݂\KO/E؟>OZʏu(}9>‡tЏq\oW'jNc#|&eoE'%J"""rž%%/矗SG׮`r]{D8 .n!'x$*ߝ>GEDDd›|v"Vz|nx/  MPt8?_ë;VuӳE8l/"""23 v{ڦ:v6xZS$"""rp8dee@cc#UUUgƞ>deedee1q3q}(--8:::FF{ƣHDDDW222 -QJ}+>a>@!; o.PGbDED&|> -rv&gNvks魸{cZ7~ _Lr'8C= [D> 4o}4M c=sOg|< Kox-Xw0c!_^ܺxrS9&fKhR`?EyZOç^巪b~?qF|6_jdwD[kW񜻃:j07w#4>Vv+z^+\E58)E?v7t(KypLX36ḵ7|:kK7\k>,~S>>R6ٺu?g֭ ? os ^ZAEФs4>1t~#*~`S&e'ًy̎p6~T|ϕ7`+[,i~*\|2 ~\^OWhDߪ~؄oO8~cij>_T Væ~%#u^G O ӊޖ .zbrp1  U^ƙqfe|ڛkx+yח39V0uPӜ}A&7eݝ6%yt7{lk0q,rwrG,[6Ogbcq٨pF 6󦃹V&?m_K^z 2,2kߪ~DoO4~Kfgpv 8\1zdFGN:q,49=~\^nOQ}N nr\xc7DOft%h8 caǛ3kO|*?/1c06m; e+F^k>b3hQ6cݑ,pHHlGD֤zp9D8 .a!'r#V&f{+HJ^οʩ#c[[NYcmkaV&\~,d_ua|~_[v^H""^>E5qSIDATc%._?=[OFopEG 0&yʄ7E8_.n9#YՏg"F$͠^.LmY-U ߪxV \|tcǠ*poZJ*(I,n޼Rͪ8܃;ޫGDM>}GD_Hn#`lfVN;cov%|a!OLB}mTyaa_a~w(i5bx~6> ƽ E;|Fv5 PSiݼSZߏ< `?G0cLƬ:87 X~]q 1o[b˅:jsbnJ[􉉉߿?{OLpeUh̄`?-$\YsLZzCy"ySZ8L;ﶄ 7ZܧHDDDPR$"""()JDDDD%E"""""@IHDDDPR$"""()JDDDD%E"""""@IHDDDPR$"""()JDDDD%E"""""@IHDDDPR$"""() -0"k -񭯅d?CKDDD&"NDD>S$""""@IHDDDPR$"""()=;"""rS$"""~.G=E""""()ᾧ=djr{wZt{ - i]o/""rSOJDDDD%E""""@)~O4HDDD%E"""""@HDDD3("rwHDDDoIDdzS$""""@I{CD?>|hM"퇡Et7ӝzDDDDsOLgAv=}PO"yљݥ]HDDD3={PO)vOt˽A=E""""HDD7ݞ^"JDDDD%E""""" -mZϡcgCغu+mmm$&&b@=E""""" /o4<֢9d5ÞK8:Zqګګj/"֤MX7^S9:8-Ljjwxܯs6szC!gyz[t2ګW()jCmS{Wo3S$"L_^oBIW{Wo0@?!"""̇ QR$""""@IHDDDPR$"""()f\>4 G)i yx83\8=j/7\HT!JCkψb;I:Ǚ6QyViyj5vq>a`}ݽ3a-L?9Ρn^}. Ro?(K}GLz3-#&K)Ʒ҈Y̎V?#iĉ4 RqU*X|gs8b")df[f ^/r26I^H]:ɽxx 7&֩;2FdV`^J4`'5NqJ+Sڼ&Y{z9[$df&)Õ}orj3[WW~]pM*eԦ#&8^WvƱZ-&J~jhMDbc ʎ>NR ٱ9G<i-="#蹬YE4j&mwyB~ӭaaTz,h9l&c_E|>ۊC$U0ֽ"~lY{sfB$•ȒH>ľ{ !av-;'3-I:ȮC-crXu+oszu42mw>)LG1A 4S`mb FenD ZHQK "җ*.^f?#fLT (( _hwe3YT^wIV X]Ȉfg^Ɂubxc̰ǻ{[;ICfn:ӟ@D:[7c+0Sִ9T^4bHOsR_;+ףXxݞ^qm^IONa5iCSo̵>'~7i"K$cbbQcΖɗpb :ny#YK(Gv$?}=5Ap_:["Y޽aN :>nVng]\9>HˀBV&QU8;i17ezlX񐐷 bp;)=Ef+WJ;xhfJp}5ˆaj;;3ź7-}Xg؄?#y b8zM)ڱ+9\m}P&#~Z x,Ԝ/v4Fsneq,{OVXc6X?2INxu0_Sm1Ļ=3/w;Yt坝"UVVMpg36&GwCOZl#tU5ox<# GEA.p.ed00HK}+˲m)$?Pe#F\3ISWҎ&t\=s?ܥF? χlt%]~FOߐۇaZבu 9갘x,ây>.]Jˀ (@HOrΦȻSK">TՕr`\n꾷8j+37zw:dF(N_n#6y_O_elJQ@*>V: 0"..{I%""rsllLA[j ;R5g]sypb1~[g"@ȑ?@6"iIMel."rq=Ee LrN\ e[ZJYJ&am^.{Z璷`npQq+  Ago'w;K~xf-,閰-{o}=^<@ LMX?_gKͧn*:@xL|Lj&nF8RYZIl04?Lȿm)ά56ϚMP gclF<%͘QDFdsn̳Ĩu,>#&9b& p*& VPz} g'`4ڻk^:Άŭ{F2h#~&7=Eŀ1a7m'Fh߿ɤȓ-R>t\lňm(:# ;oO3A.v,#X]ä*%W9NdѦl\FMmh&'-v5?H\m7?}8Am?=0ב:y^k๵Å ۟Ԯ3l^H5`$6.MYJvlνmyDS 4]ٳYFLw}]exq*>i 67.:{q@A5h'q?@;'}pzԴ`޿MϤ1niUFHsG|2 =\ng 60il$0Ȁ?H08614!*%(1L񌩟W8ɃI]4ڻ5Ҽ`gbwY?k??%^z%^2Ƅh5ykɈ%QcrAJ8xMkɊ ܈aˎ+&e󣨯iKdd' mȞA^J*Y,('YdD-cVfEeEYIvvS_;±>2ck6 V_\ȪEÓ̒¹ \Jm4w$3/7"Җ1ca~G8RY~ >a dWqn:3bYa%:qz86Lj`E. ӛvQ_zho{Yh㣡xǗgvoYv;ٰq1].> nCk8^<*O 6R^3O6 U9xɥp^g6 mB񉡥o&t]> lbjD73VɌGY.w^"~&ܿM&"&Os8+7lC )JQ "R._b{0kܿ96?n[kvQ|g,7zrx`[&{Qis}G;Cl_wrܧr/UOR.,Ԃ; OepsM sblBGGaρ"_^Z>`=g% Rq*>Xhܾ'Htox;Uaz6*ZZ*v38xngLo"6YOQFF .ʨ|+.ӣtv`LtzKtt4466 }C/E`gϒEZZUUUTUUMģ"y TTTPQa>{=E""""w""s s>IENDB`kew-2.8.2/include/000077500000000000000000000000001467402032100137335ustar00rootroot00000000000000kew-2.8.2/include/imgtotxt/000077500000000000000000000000001467402032100156125ustar00rootroot00000000000000kew-2.8.2/include/imgtotxt/LICENSE000066400000000000000000000020561467402032100166220ustar00rootroot00000000000000MIT License Copyright (c) 2021 Danny Burrows Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. kew-2.8.2/include/imgtotxt/options.h000066400000000000000000000011261467402032100174560ustar00rootroot00000000000000#ifndef OPTIONS_H #define OPTIONS_H // Some preprocessor magic to generate an enum and string array with the same items. #define OUTPUT_MODES(MODE) \ MODE(ANSI) \ MODE(SOLID_ANSI) \ MODE(ASCII) #define GENERATE_ENUM(ENUM) ENUM, #define GENERATE_STRING(STRING) #STRING, enum OutputModes { OUTPUT_MODES(GENERATE_ENUM) }; typedef struct { unsigned int width; unsigned int height; enum OutputModes output_mode; bool original_size; bool true_color; bool squashing_enabled; bool suppress_header; } ImageOptions; #endif /* OPTIONS_H */ kew-2.8.2/include/imgtotxt/write_ascii.c000066400000000000000000000111431467402032100202600ustar00rootroot00000000000000/* TODO: - Debug arguments parsing behaviour. - Options at beginning or end applied to all unless specified otherwise? - Improve 'scale' for plane ASCII output. - Preserve aspect-ratio option. - Check if 256 colors is default. - Check if rbg colors are widely supported. - Translating from rgb into 256 colors. - Windows support? Modified, originally by Danny Burrows: https://github.com/danny-burrows/img_to_txt */ #include #include #include #include "../../src/term.h" // Disable some warnings for stb headers. #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wcast-qual" #pragma GCC diagnostic ignored "-Wstrict-overflow" #define STB_IMAGE_IMPLEMENTATION #include #define STB_IMAGE_RESIZE_IMPLEMENTATION #include #pragma GCC diagnostic pop #include "options.h" #include "write_ascii.h" #define MAX_IMG_SIZE 25000 #define MACRO_STRLEN(s) (sizeof(s) / sizeof(s[0])) bool brightPixelFound; char scale[] = "$@&B%8WM#ZO0QoahkbdpqwmLCJUYXIjft/\\|()1{}[]l?zcvunxr!<>i;:*-+~_,\"^`'. "; unsigned int brightness_levels = MACRO_STRLEN(scale) - 2; unsigned char luminanceFromRGB(unsigned char r, unsigned char g, unsigned char b) { return (unsigned char)(0.2126 * r + 0.7152 * g + 0.0722 * b); } unsigned char calc_ascii_char(PixelData *p) { // Calc luminace and use to find Ascii char. unsigned char ch = luminanceFromRGB(p->r, p->g, p->b); int rescaled = ch * brightness_levels / 256; return scale[brightness_levels - rescaled]; } int read_and_convert(const char *filepath, ImageOptions *options) { int rwidth, rheight, rchannels; unsigned char *read_data = stbi_load(filepath, &rwidth, &rheight, &rchannels, 3); if (read_data == NULL) { fprintf(stderr, "Error reading image data!\n\n"); return -1; } unsigned int desired_width, desired_height; desired_width = options->width; desired_height = options->height; // Check for and do any needed image resizing... PixelData *data; if (desired_width != (unsigned)rwidth || desired_height != (unsigned)rheight) { // 3 * uint8 for RGB! unsigned char *new_data = malloc(3 * sizeof(unsigned char) * desired_width * desired_height); int r = stbir_resize_uint8( read_data, rwidth, rheight, 0, new_data, desired_width, desired_height, 0, 3); if (r == 0) { perror("Error resizing image:"); return -1; } stbi_image_free(read_data); // Free read_data. data = (PixelData *)new_data; } else { data = (PixelData *)read_data; } int term_w, term_h; getTermSize(&term_w, &term_h); int indent = ((term_w - desired_width) / 2)+1; if (!options->suppress_header) printf("\n\r"); printf("\n"); printf("%*s", indent, ""); for (unsigned int d = 0; d < desired_width * desired_height; d++) { if (d % desired_width == 0 && d != 0) { if (options->output_mode == SOLID_ANSI) printf("\033[0m"); printf("\n"); printf("%*s", indent, ""); } PixelData *c = data + d; switch (options->output_mode) { case ASCII: printf("%c", calc_ascii_char(c)); break; case ANSI: printf("\033[1;38;2;%03u;%03u;%03um%c", c->r, c->g, c->b, calc_ascii_char(c)); break; case SOLID_ANSI: printf("\033[48;2;%03u;%03u;%03um ", c->r, c->g, c->b); calc_ascii_char(c); break; default: break; } } if (options->output_mode == SOLID_ANSI) printf("\033[0m"); printf("\n"); stbi_image_free(data); return 0; } int output_ascii(const char *pathToImgFile, int height, int width) { ImageOptions opts = { .output_mode = ANSI, .original_size = false, .true_color = true, .squashing_enabled = true, .suppress_header = true, }; if (width > MAX_IMG_SIZE) { fprintf(stderr, "[ERR] Width exceeds maximum image size!\n"); return -1; } opts.width = width--; // compensate, first character on each line is going to be blank if (height > MAX_IMG_SIZE) { fprintf(stderr, "[ERR] Height exceeds maximum image size!\n"); return -1; } opts.height = height; printf("\r"); int ret = read_and_convert(pathToImgFile, &opts); if (ret == -1) // fprintf(stderr, "Failed to convert image: %s\n", pathToImgFile); printf("\033[0m"); return 0; } kew-2.8.2/include/imgtotxt/write_ascii.h000066400000000000000000000010061467402032100202620ustar00rootroot00000000000000#ifndef C_H #define C_H #include #include "options.h" /* This ifdef allows the header to be used from both C and C++. */ #ifdef __cplusplus extern "C" { #endif #ifndef PIXELDATA_STRUCT #define PIXELDATA_STRUCT typedef struct { unsigned char r; unsigned char g; unsigned char b; } PixelData; #endif int getBrightPixel(char *filepath, int width, int height); int output_ascii(const char *pathToImgFile, int height, int width); #ifdef __cplusplus } #endif #endif kew-2.8.2/install.sh000066400000000000000000000054431467402032100143200ustar00rootroot00000000000000#!/bin/bash # Check if the script is running as root if [[ $EUID -ne 0 ]]; then echo "This script must be run as root." exit 1 fi # Removing old files if exist if [ -d "kew" ]; then echo "Removing old files" rm -rf kew &>/dev/null fi # Install dependencies based on the package manager available echo "Installing missing dependencies" if command -v apt &>/dev/null; then apt install -y pkg-config ffmpeg libfftw3-dev libopus-dev libopusfile-dev libvorbis-dev git gcc make libchafa-dev libfreeimage-dev libavformat-dev libnotify-dev elif command -v dnf &>/dev/null; then dnf install -y pkg-config ffmpeg-free-devel fftw-devel opus-devel opusfile-devel libvorbis-devel git gcc make chafa-devel freeimage-devel libavformat-free-devel libnotify-devel libatomic elif command -v yum &>/dev/null; then yum install -y pkgconfig ffmpeg fftw-devel opus-devel opusfile-devel libvorbis-devel git gcc make chafa-devel libfreeimage-devel libavformat-devel libnotify-devel elif command -v pacman &>/dev/null; then pacman -Syu --noconfirm --needed pkg-config ffmpeg fftw git gcc make chafa freeimage opus opusfile libvorbis libnotify elif command -v zypper &>/dev/null; then zypper install -y pkg-config ffmpeg fftw-devel opus-devel opusfile-devel libvorbis-devel git chafa-devel gcc make libfreeimage-devel libavformat-devel libnotify-devel elif command -v eopkg &>/dev/null; then eopkg install -y pkg-config ffmpeg fftw-devel opus-devel opusfile-devel libvorbis-devel git gcc make chafa-devel libfreeimage-devel libavformat-devel libnotify-devel elif command -v guix &>/dev/null; then guix install pkg-config ffmpeg fftw git gcc make chafa freeimage libavformat opus opusfile libvorbis libnotify elif command -v xbps-install &>/dev/null; then xbps-install -y pkg-config ffmpeg fftw git gcc make chafa libfreeimage libavformat opus opusfile libvorbis libnotify-devel else echo "Unsupported package manager. Please install the required dependencies manually." exit 1 fi # Clone the repository repo_url="https://github.com/ravachol/kew.git" echo "Cloning the repository..." if git clone "$repo_url" --depth=1 &>/dev/null; then echo "Repository cloned successfully." else echo "Failed to clone the repository. Please check your network connection and try again." exit 1 fi # Changing directory cd kew # Building echo "Building the project..." if make &> build.log; then echo "Build completed successfully." else echo "Build encountered an error. Please check the build.log file for more information." exit 1 fi # Installing echo "Installing the project..." if sudo make install; then echo "Installation completed successfully." else echo "Installation encountered an error." exit 1 fi # Cleaning up the directory echo "Cleaning directory..." cd .. rm kew -rf &>/dev/null kew-2.8.2/src/000077500000000000000000000000001467402032100130775ustar00rootroot00000000000000kew-2.8.2/src/cache.c000066400000000000000000000023511467402032100143070ustar00rootroot00000000000000#define _XOPEN_SOURCE 700 #include "cache.h" /* cache.c Related to cache which contains paths to cached files. */ Cache *createCache() { Cache *cache = (Cache *)malloc(sizeof(Cache)); cache->head = NULL; return cache; } void addToCache(Cache *cache, const char *filePath) { CacheNode *newNode = (CacheNode *)malloc(sizeof(CacheNode)); newNode->filePath = strdup(filePath); newNode->next = cache->head; cache->head = newNode; } void deleteCache(Cache *cache) { if (cache) { CacheNode *current = cache->head; while (current != NULL) { CacheNode *temp = current; current = current->next; free(temp->filePath); free(temp); } free(cache); } } bool existsInCache(Cache *cache, char *filePath) { CacheNode *current = cache->head; while (current != NULL) { if (strcmp(filePath, current->filePath) == 0) { return true; } current = current->next; } return false; } kew-2.8.2/src/cache.h000066400000000000000000000006711467402032100143170ustar00rootroot00000000000000#ifndef CACHE_H #define CACHE_H #include #include #include #include typedef struct CacheNode { char *filePath; struct CacheNode *next; } CacheNode; typedef struct Cache { CacheNode *head; } Cache; Cache *createCache(void); void addToCache(Cache *cache, const char *filePath); void deleteCache(Cache *cache); bool existsInCache(Cache *cache, char *filePath); #endif kew-2.8.2/src/chafafunc.c000066400000000000000000000443631467402032100151730ustar00rootroot00000000000000#include "chafafunc.h" /* chafafunc.c Functions related to printing images to the terminal with chafa. */ /* Include after chafa.h for G_OS_WIN32 */ #ifdef G_OS_WIN32 #ifdef HAVE_WINDOWS_H #include #endif #include #else #include /* ioctl */ #endif typedef struct { gint width_cells, height_cells; gint width_pixels, height_pixels; } TermSize; static void detect_terminal(ChafaTermInfo **term_info_out, ChafaCanvasMode *mode_out, ChafaPixelMode *pixel_mode_out) { ChafaCanvasMode mode; ChafaPixelMode pixel_mode; ChafaTermInfo *term_info; gchar **envp; /* Examine the environment variables and guess what the terminal can do */ envp = g_get_environ(); term_info = chafa_term_db_detect(chafa_term_db_get_default(), envp); /* See which control sequences were defined, and use that to pick the most * high-quality rendering possible */ if (chafa_term_info_have_seq(term_info, CHAFA_TERM_SEQ_BEGIN_KITTY_IMMEDIATE_IMAGE_V1)) { pixel_mode = CHAFA_PIXEL_MODE_KITTY; mode = CHAFA_CANVAS_MODE_TRUECOLOR; } else if (chafa_term_info_have_seq(term_info, CHAFA_TERM_SEQ_BEGIN_SIXELS)) { pixel_mode = CHAFA_PIXEL_MODE_SIXELS; mode = CHAFA_CANVAS_MODE_TRUECOLOR; } else { pixel_mode = CHAFA_PIXEL_MODE_SYMBOLS; if (chafa_term_info_have_seq(term_info, CHAFA_TERM_SEQ_SET_COLOR_FGBG_DIRECT) && chafa_term_info_have_seq(term_info, CHAFA_TERM_SEQ_SET_COLOR_FG_DIRECT) && chafa_term_info_have_seq(term_info, CHAFA_TERM_SEQ_SET_COLOR_BG_DIRECT)) mode = CHAFA_CANVAS_MODE_TRUECOLOR; else if (chafa_term_info_have_seq(term_info, CHAFA_TERM_SEQ_SET_COLOR_FGBG_256) && chafa_term_info_have_seq(term_info, CHAFA_TERM_SEQ_SET_COLOR_FG_256) && chafa_term_info_have_seq(term_info, CHAFA_TERM_SEQ_SET_COLOR_BG_256)) mode = CHAFA_CANVAS_MODE_INDEXED_240; else if (chafa_term_info_have_seq(term_info, CHAFA_TERM_SEQ_SET_COLOR_FGBG_16) && chafa_term_info_have_seq(term_info, CHAFA_TERM_SEQ_SET_COLOR_FG_16) && chafa_term_info_have_seq(term_info, CHAFA_TERM_SEQ_SET_COLOR_BG_16)) mode = CHAFA_CANVAS_MODE_INDEXED_16; else if (chafa_term_info_have_seq(term_info, CHAFA_TERM_SEQ_INVERT_COLORS) && chafa_term_info_have_seq(term_info, CHAFA_TERM_SEQ_RESET_ATTRIBUTES)) mode = CHAFA_CANVAS_MODE_FGBG_BGFG; else mode = CHAFA_CANVAS_MODE_FGBG; } /* Hand over the information to caller */ *term_info_out = term_info; *mode_out = mode; *pixel_mode_out = pixel_mode; /* Cleanup */ g_strfreev(envp); } static void get_tty_size(TermSize *term_size_out) { TermSize term_size; term_size.width_cells = term_size.height_cells = term_size.width_pixels = term_size.height_pixels = -1; #ifdef G_OS_WIN32 { HANDLE chd = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_SCREEN_BUFFER_INFO csb_info; if (chd != INVALID_HANDLE_VALUE && GetConsoleScreenBufferInfo(chd, &csb_info)) { term_size.width_cells = csb_info.srWindow.Right - csb_info.srWindow.Left + 1; term_size.height_cells = csb_info.srWindow.Bottom - csb_info.srWindow.Top + 1; } } #else { struct winsize w; gboolean have_winsz = FALSE; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) >= 0 || ioctl(STDERR_FILENO, TIOCGWINSZ, &w) >= 0 || ioctl(STDIN_FILENO, TIOCGWINSZ, &w) >= 0) have_winsz = TRUE; if (have_winsz) { term_size.width_cells = w.ws_col; term_size.height_cells = w.ws_row; term_size.width_pixels = w.ws_xpixel; term_size.height_pixels = w.ws_ypixel; } } #endif if (term_size.width_cells <= 0) term_size.width_cells = -1; if (term_size.height_cells <= 2) term_size.height_cells = -1; /* If .ws_xpixel and .ws_ypixel are filled out, we can calculate * aspect information for the font used. Sixel-capable terminals * like mlterm set these fields, but most others do not. */ if (term_size.width_pixels <= 0 || term_size.height_pixels <= 0) { term_size.width_pixels = -1; term_size.height_pixels = -1; } *term_size_out = term_size; } static void tty_init(void) { #ifdef G_OS_WIN32 { HANDLE chd = GetStdHandle(STD_OUTPUT_HANDLE); saved_console_output_cp = GetConsoleOutputCP(); saved_console_input_cp = GetConsoleCP(); /* Enable ANSI escape sequence parsing etc. on MS Windows command prompt */ if (chd != INVALID_HANDLE_VALUE) { if (!SetConsoleMode(chd, ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN)) win32_stdout_is_file = TRUE; } /* Set UTF-8 code page I/O */ SetConsoleOutputCP(65001); SetConsoleCP(65001); } #endif } static GString * convert_image(const void *pixels, gint pix_width, gint pix_height, gint pix_rowstride, ChafaPixelType pixel_type, gint width_cells, gint height_cells, gint cell_width, gint cell_height) { ChafaTermInfo *term_info; ChafaCanvasMode mode; ChafaPixelMode pixel_mode; ChafaSymbolMap *symbol_map; ChafaCanvasConfig *config; ChafaCanvas *canvas; GString *printable; detect_terminal(&term_info, &mode, &pixel_mode); /* Specify the symbols we want */ symbol_map = chafa_symbol_map_new(); chafa_symbol_map_add_by_tags(symbol_map, CHAFA_SYMBOL_TAG_BLOCK); /* Set up a configuration with the symbols and the canvas size in characters */ config = chafa_canvas_config_new(); chafa_canvas_config_set_canvas_mode(config, mode); chafa_canvas_config_set_pixel_mode(config, pixel_mode); chafa_canvas_config_set_geometry(config, width_cells, height_cells); chafa_canvas_config_set_symbol_map(config, symbol_map); if (cell_width > 0 && cell_height > 0) { /* We know the pixel dimensions of each cell. Store it in the config. */ chafa_canvas_config_set_cell_geometry(config, cell_width, cell_height); } /* Create canvas */ canvas = chafa_canvas_new(config); /* Draw pixels to the canvas */ chafa_canvas_draw_all_pixels(canvas, pixel_type, pixels, pix_width, pix_height, pix_rowstride); /* Build printable string */ printable = chafa_canvas_print(canvas, term_info); /* Clean up and return */ chafa_canvas_unref(canvas); chafa_canvas_config_unref(config); chafa_symbol_map_unref(symbol_map); chafa_term_info_unref(term_info); canvas = NULL; config = NULL; symbol_map = NULL; term_info = NULL; return printable; } void printImage(const char *image_path, int width, int height) { FreeImage_Initialise(false); // Detect the image type FREE_IMAGE_FORMAT image_format = FreeImage_GetFileType(image_path, 0); if (image_format == FIF_UNKNOWN) { // Failed to detect the image type printf("Unknown image format: %s\n", image_path); return; } // Load the image FIBITMAP *image = FreeImage_Load(image_format, image_path, 0); if (!image) { // Failed to load the image printf("Failed to load image: %s\n", image_path); return; } // Convert the image to a bitmap FIBITMAP *bitmap = FreeImage_ConvertTo32Bits(image); if (!bitmap) { // Failed to convert the image to a bitmap printf("Failed to convert image to bitmap.\n"); FreeImage_Unload(image); return; } int pix_width = FreeImage_GetWidth(bitmap); int pix_height = FreeImage_GetHeight(bitmap); int n_channels = FreeImage_GetBPP(bitmap) / 8; unsigned char *pixels = (unsigned char *)FreeImage_GetBits(bitmap); FreeImage_FlipVertical(bitmap); TermSize term_size; GString *printable; gfloat font_ratio = 0.5; gint cell_width = -1, cell_height = -1; gint width_cells, height_cells; tty_init(); get_tty_size(&term_size); if (term_size.width_cells > 0 && term_size.height_cells > 0 && term_size.width_pixels > 0 && term_size.height_pixels > 0) { cell_width = term_size.width_pixels / term_size.width_cells; cell_height = term_size.height_pixels / term_size.height_cells; font_ratio = (gdouble)cell_width / (gdouble)cell_height; } width_cells = term_size.width_cells; height_cells = term_size.height_cells; chafa_calc_canvas_geometry(pix_width, pix_height, &width_cells, &height_cells, font_ratio, TRUE, FALSE); /* Convert the image to a printable string */ printable = convert_image(pixels, pix_width, pix_height, pix_width * n_channels, CHAFA_PIXEL_BGRA8_UNASSOCIATED, width, height, cell_width, cell_height); /* Print the string */ fwrite(printable->str, sizeof(char), printable->len, stdout); fputc('\n', stdout); // Free resources FreeImage_Unload(bitmap); FreeImage_Unload(image); g_string_free(printable, TRUE); } FIBITMAP *getBitmap(const char *image_path) { if (image_path == NULL) return NULL; FreeImage_Initialise(false); FREE_IMAGE_FORMAT image_format = FreeImage_GetFileType(image_path, 0); if (image_format == FIF_UNKNOWN) { return NULL; } FIBITMAP *image = FreeImage_Load(image_format, image_path, 0); if (!image) { return NULL; } FIBITMAP *bitmap = FreeImage_ConvertTo32Bits(image); FreeImage_FlipVertical(bitmap); FreeImage_Unload(image); if (!bitmap) { return NULL; } return bitmap; } void printBitmap(FIBITMAP *bitmap, int width, int height) { if (bitmap == NULL) { return; } int pix_width = FreeImage_GetWidth(bitmap); int pix_height = FreeImage_GetHeight(bitmap); int n_channels = FreeImage_GetBPP(bitmap) / 8; unsigned char *pixels = (unsigned char *)FreeImage_GetBits(bitmap); TermSize term_size; GString *printable; gfloat font_ratio = 0.5; gint cell_width = -1, cell_height = -1; gint width_cells, height_cells; tty_init(); get_tty_size(&term_size); if (term_size.width_cells > 0 && term_size.height_cells > 0 && term_size.width_pixels > 0 && term_size.height_pixels > 0) { cell_width = term_size.width_pixels / term_size.width_cells; cell_height = term_size.height_pixels / term_size.height_cells; font_ratio = (gdouble)cell_width / (gdouble)cell_height; } width_cells = term_size.width_cells; height_cells = term_size.height_cells; chafa_calc_canvas_geometry(pix_width, pix_height, &width_cells, &height_cells, font_ratio, TRUE, FALSE); // Convert image to a printable string printable = convert_image(pixels, pix_width, pix_height, pix_width * n_channels, CHAFA_PIXEL_BGRA8_UNASSOCIATED, width, height, cell_width, cell_height); fwrite(printable->str, sizeof(char), printable->len, stdout); g_string_free(printable, TRUE); } float calcAspectRatio() { TermSize term_size; gint cell_width = -1, cell_height = -1; tty_init(); get_tty_size(&term_size); if (term_size.width_cells > 0 && term_size.height_cells > 0 && term_size.width_pixels > 0 && term_size.height_pixels > 0) { cell_width = term_size.width_pixels / term_size.width_cells; cell_height = term_size.height_pixels / term_size.height_cells; } // Set default for some terminals if (cell_width == -1 && cell_height == -1) { cell_width = 8; cell_height = 16; } return (float)cell_height / (float)cell_width; } void printSquareBitmapCentered(FIBITMAP *bitmap, int baseHeight) { if (bitmap == NULL) { return; } int pix_width = FreeImage_GetWidth(bitmap); int pix_height = FreeImage_GetHeight(bitmap); int n_channels = FreeImage_GetBPP(bitmap) / 8; unsigned char *pixels = (unsigned char *)FreeImage_GetBits(bitmap); TermSize term_size; GString *printable; gint cell_width = -1, cell_height = -1; tty_init(); get_tty_size(&term_size); if (term_size.width_cells > 0 && term_size.height_cells > 0 && term_size.width_pixels > 0 && term_size.height_pixels > 0) { cell_width = term_size.width_pixels / term_size.width_cells; cell_height = term_size.height_pixels / term_size.height_cells; } // Set default for some terminals if (cell_width == -1 && cell_height == -1) { cell_width = 8; cell_height = 16; } float aspect_ratio_correction = (float)cell_height / (float)cell_width; int correctedWidth = (int)(baseHeight * aspect_ratio_correction); // Convert image to a printable string printable = convert_image(pixels, pix_width, pix_height, pix_width * n_channels, CHAFA_PIXEL_BGRA8_UNASSOCIATED, correctedWidth, baseHeight, cell_width, cell_height); g_string_append_c(printable, '\0'); const gchar *delimiters = "\n"; gchar **lines = g_strsplit(printable->str, delimiters, -1); int indentation = ((term_size.width_cells - correctedWidth) / 2) + 1; for (int i = 0; lines[i] != NULL; i++) { printf("\n%*s%s", indentation, "", lines[i]); } g_strfreev(lines); g_string_free(printable, TRUE); } void printBitmapCentered(FIBITMAP *bitmap, int width, int height) { if (bitmap == NULL) { return; } int pix_width = FreeImage_GetWidth(bitmap); int pix_height = FreeImage_GetHeight(bitmap); int n_channels = FreeImage_GetBPP(bitmap) / 8; unsigned char *pixels = (unsigned char *)FreeImage_GetBits(bitmap); TermSize term_size; GString *printable; gint cell_width = -1, cell_height = -1; tty_init(); get_tty_size(&term_size); if (term_size.width_cells > 0 && term_size.height_cells > 0 && term_size.width_pixels > 0 && term_size.height_pixels > 0) { cell_width = term_size.width_pixels / term_size.width_cells; cell_height = term_size.height_pixels / term_size.height_cells; } // Convert image to a printable string printable = convert_image(pixels, pix_width, pix_height, pix_width * n_channels, CHAFA_PIXEL_BGRA8_UNASSOCIATED, width, height, cell_width, cell_height); g_string_append_c(printable, '\0'); const gchar *delimiters = "\n"; gchar **lines = g_strsplit(printable->str, delimiters, -1); int indentation = ((term_size.width_cells - width) / 2) + 1; for (int i = 0; lines[i] != NULL; i++) { printf("\n%*s%s", indentation, "", lines[i]); } g_strfreev(lines); g_string_free(printable, TRUE); } unsigned char luminance(unsigned char r, unsigned char g, unsigned char b) { return (unsigned char)(0.2126 * r + 0.7152 * g + 0.0722 * b); } void checkIfBrightPixel(unsigned char r, unsigned char g, unsigned char b, bool *found) { // Calc luminace and use to find Ascii char. unsigned char ch = luminance(r, g, b); if (ch > 80 && !(r < g + 20 && r > g - 20 && g < b + 20 && g > b - 20) && !(r > 150 && g > 150 && b > 150)) { *found = true; } } int getCoverColor(FIBITMAP *bitmap, unsigned char *r, unsigned char *g, unsigned char *b) { int rwidth = FreeImage_GetWidth(bitmap); int rheight = FreeImage_GetHeight(bitmap); int rchannels = FreeImage_GetBPP(bitmap) / 8; unsigned char *read_data = (unsigned char *)FreeImage_GetBits(bitmap); if (read_data == NULL) { return -1; } bool found = false; int numPixels = rwidth * rheight; for (int i = 0; i < numPixels; i++) { int index = i * rchannels; unsigned char blue = 0; unsigned char green = 0; unsigned char red = 0; if (rchannels >= 3) { blue = read_data[index + 0]; green = read_data[index + 1]; red = read_data[index + 2]; } else if (rchannels >= 1) { blue = green = red = read_data[index]; } checkIfBrightPixel(red, green, blue, &found); if (found) { *(r) = red; *(g) = green; *(b) = blue; break; } } return 0; } kew-2.8.2/src/chafafunc.h000066400000000000000000000011211467402032100151610ustar00rootroot00000000000000#include #include #include #include #include #include #include #include float calcAspectRatio(); void printImage(const char *image_path, int width, int height); FIBITMAP *getBitmap(const char *image_path); void printBitmap(FIBITMAP *bitmap, int width, int height); void printBitmapCentered(FIBITMAP *bitmap, int width, int height); void printSquareBitmapCentered(FIBITMAP *bitmap, int baseHeight); int getCoverColor(FIBITMAP *bitmap, unsigned char *r, unsigned char *g, unsigned char *b); kew-2.8.2/src/common_ui.c000066400000000000000000000022041467402032100152260ustar00rootroot00000000000000 #include "common_ui.h" //0=Black, 1=Red, 2=Green, 3=Yellow, 4=Blue, 5=Magenta, 6=Cyan, 7=White int mainColor = 6; int titleColor = 6; int artistColor = 6; int enqueuedColor = 6; PixelData color = {125, 125, 125}; unsigned char defaultColor = 150; bool useProfileColors = false; void setTextColorRGB2(int r, int g, int b) { if (!useProfileColors) setTextColorRGB(r, g, b); } void setColor() { setColorAndWeight(0); } void setColorAndWeight(int bold) { if (useProfileColors) { printf("\033[%dm", bold); return; } if (color.r == defaultColor && color.g == defaultColor && color.b == defaultColor) printf("\033[%dm", bold); else if (color.r >= 210 && color.g >= 210 && color.b >= 210) { color.r = defaultColor; color.g = defaultColor; color.b = defaultColor; printf("\033[%d;38;2;%03u;%03u;%03um", bold, color.r, color.g, color.b); } else { printf("\033[%d;38;2;%03u;%03u;%03um", bold, color.r, color.g, color.b); } } kew-2.8.2/src/common_ui.h000066400000000000000000000006701467402032100152400ustar00rootroot00000000000000#include #include "term.h" #include "../include/imgtotxt/write_ascii.h" //0=Black, 1=Red, 2=Green, 3=Yellow, 4=Blue, 5=Magenta, 6=Cyan, 7=White extern int mainColor; extern int titleColor; extern int artistColor; extern int enqueuedColor; extern unsigned char defaultColor; extern PixelData color; extern bool useProfileColors; void setTextColorRGB2(int r, int g, int b); void setColor(); void setColorAndWeight(int bold); kew-2.8.2/src/directorytree.c000066400000000000000000000423101467402032100161270ustar00rootroot00000000000000#include "directorytree.h" static int lastUsedId = 0; typedef void (*TimeoutCallback)(void); FileSystemEntry *createEntry(const char *name, int isDirectory, FileSystemEntry *parent) { FileSystemEntry *newEntry = (FileSystemEntry *)malloc(sizeof(FileSystemEntry)); if (newEntry != NULL) { newEntry->name = strdup(name); newEntry->isDirectory = isDirectory; newEntry->isEnqueued = 0; newEntry->parent = parent; newEntry->children = NULL; newEntry->next = NULL; newEntry->id = ++lastUsedId; if (parent != NULL) { newEntry->parentId = parent->id; } else { newEntry->parentId = -1; } } return newEntry; } void addChild(FileSystemEntry *parent, FileSystemEntry *child) { if (parent != NULL) { child->next = parent->children; parent->children = child; } } void setFullPath(FileSystemEntry *entry, const char *parentPath, const char *entryName) { if (entry == NULL || parentPath == NULL || entryName == NULL) { return; } size_t fullPathLength = strlen(parentPath) + strlen(entryName) + 2; // +2 for '/' and '\0' entry->fullPath = (char *)malloc(fullPathLength); if (entry->fullPath == NULL) { return; } snprintf(entry->fullPath, fullPathLength, "%s/%s", parentPath, entryName); } void displayTreeSimple(FileSystemEntry *root, int depth) { for (int i = 0; i < depth; ++i) { printf(" "); } printf("%s", root->name); if (root->isDirectory) { printf(" (Directory)\n"); FileSystemEntry *child = root->children; while (child != NULL) { displayTreeSimple(child, depth + 1); child = child->next; } } else { printf(" (File)\n"); } } void freeTree(FileSystemEntry *root) { if (root == NULL) { return; } FileSystemEntry *child = root->children; while (child != NULL) { FileSystemEntry *next = child->next; freeTree(child); child = next; } free(root->name); free(root->fullPath); free(root); } int removeEmptyDirectories(FileSystemEntry *node) { if (node == NULL) { return 0; } FileSystemEntry *currentChild = node->children; FileSystemEntry *prevChild = NULL; int numEntries = 0; while (currentChild != NULL) { if (currentChild->isDirectory) { numEntries += removeEmptyDirectories(currentChild); if (currentChild->children == NULL) { if (prevChild == NULL) { node->children = currentChild->next; } else { prevChild->next = currentChild->next; } FileSystemEntry *toFree = currentChild; currentChild = currentChild->next; free(toFree->name); free(toFree->fullPath); free(toFree); numEntries++; continue; } } prevChild = currentChild; currentChild = currentChild->next; } return numEntries; } char *stringToUpperWithoutSpaces(const char *str) { if (str == NULL) { return NULL; } size_t len = strlen(str); char *result = (char *)malloc(len + 1); if (result == NULL) { return NULL; } size_t resultIndex = 0; for (size_t i = 0; i < len; ++i) { if (!isspace((unsigned char)str[i])) { result[resultIndex++] = toupper((unsigned char)str[i]); } } result[resultIndex] = '\0'; return result; } int compareLibEntries(const struct dirent **a, const struct dirent **b) { char *nameA = stringToUpperWithoutSpaces((*a)->d_name); char *nameB = stringToUpperWithoutSpaces((*b)->d_name); if (nameA[0] == '_' && nameB[0] != '_') { free(nameA); free(nameB); return 1; } else if (nameA[0] != '_' && nameB[0] == '_') { free(nameA); free(nameB); return -1; } int result = strcmp(nameB, nameA); free(nameA); free(nameB); return result; } int readDirectory(const char *path, FileSystemEntry *parent) { DIR *directory = opendir(path); if (directory == NULL) { perror("Error opening directory"); return 0; } struct dirent **entries; int dirEntries = scandir(path, &entries, NULL, compareLibEntries); if (dirEntries < 0) { perror("Error scanning directory entries"); closedir(directory); return 0; } regex_t regex; regcomp(®ex, AUDIO_EXTENSIONS, REG_EXTENDED); int numEntries = 0; for (int i = 0; i < dirEntries; ++i) { struct dirent *entry = entries[i]; if (entry->d_name[0] != '.' && strcmp(entry->d_name, ".") != 0 && strcmp(entry->d_name, "..") != 0) { char childPath[MAXPATHLEN]; snprintf(childPath, sizeof(childPath), "%s/%s", path, entry->d_name); struct stat fileStats; if (stat(childPath, &fileStats) == -1) { continue; } int isDirectory = true; if (S_ISREG(fileStats.st_mode)) { isDirectory = false; } char exto[6]; extractExtension(entry->d_name, sizeof(exto) - 1, exto); int isAudio = match_regex(®ex, exto); if (isAudio == 0 || isDirectory) { FileSystemEntry *child = createEntry(entry->d_name, isDirectory, parent); if (entry != NULL) { setFullPath(child, path, entry->d_name); } addChild(parent, child); if (isDirectory) { numEntries++; numEntries += readDirectory(childPath, child); } } } free(entry); } free(entries); regfree(®ex); closedir(directory); return numEntries; } void writeTreeToFile(FileSystemEntry *node, FILE *file, int parentId) { if (node == NULL) { return; } fprintf(file, "%d\t%s\t%d\t%d\n", node->id, node->name, node->isDirectory, parentId); FileSystemEntry *child = node->children; FileSystemEntry *tmp = NULL; while (child) { tmp = child->next; writeTreeToFile(child, file, node->id); child = tmp; } free(node->name); free(node->fullPath); free(node); } void freeAndWriteTree(FileSystemEntry *root, const char *filename) { FILE *file = fopen(filename, "w"); if (!file) { perror("Failed to open file"); return; } writeTreeToFile(root, file, -1); fclose(file); } FileSystemEntry *createDirectoryTree(const char *startPath, int *numEntries) { FileSystemEntry *root = createEntry("root", 1, NULL); setFullPath(root, "", ""); *numEntries = readDirectory(startPath, root); *numEntries -= removeEmptyDirectories(root); lastUsedId = 0; return root; } FileSystemEntry **resizeNodesArray(FileSystemEntry **nodes, int oldSize, int newSize) { FileSystemEntry **newNodes = realloc(nodes, newSize * sizeof(FileSystemEntry *)); if (newNodes) { for (int i = oldSize; i < newSize; i++) { newNodes[i] = NULL; } } return newNodes; } FileSystemEntry *reconstructTreeFromFile(const char *filename, const char *startMusicPath, int *numDirectoryEntries) { FILE *file = fopen(filename, "r"); if (!file) { return NULL; } char line[1024]; int nodesCount = 0, nodesCapacity = 1000, oldCapacity = 0; FileSystemEntry **nodes = calloc(nodesCapacity, sizeof(FileSystemEntry *)); if (!nodes) { fclose(file); return NULL; } FileSystemEntry *root = NULL; while (fgets(line, sizeof(line), file)) { int id, parentId, isDirectory; char name[256]; if (sscanf(line, "%d\t%255[^\t]\t%d\t%d", &id, name, &isDirectory, &parentId) == 4) { if (id >= nodesCapacity) { oldCapacity = nodesCapacity; nodesCapacity = id + 100; FileSystemEntry **tempNodes = resizeNodesArray(nodes, oldCapacity, nodesCapacity); if (!tempNodes) { perror("Failed to resize nodes array"); for (int i = 0; i < nodesCount; i++) { if (nodes[i]) { free(nodes[i]->name); free(nodes[i]->fullPath); free(nodes[i]); } } free(nodes); fclose(file); exit(EXIT_FAILURE); } nodes = tempNodes; } FileSystemEntry *node = malloc(sizeof(FileSystemEntry)); if (!node) { perror("Failed to allocate node"); fclose(file); exit(EXIT_FAILURE); } node->id = id; node->name = strdup(name); node->isDirectory = isDirectory; node->isEnqueued = 0; node->children = node->next = node->parent = NULL; nodes[id] = node; nodesCount++; if (parentId >= 0 && nodes[parentId]) { node->parent = nodes[parentId]; if (nodes[parentId]->children) { FileSystemEntry *child = nodes[parentId]->children; while (child->next) { child = child->next; } child->next = node; } else { nodes[parentId]->children = node; } setFullPath(node, nodes[parentId]->fullPath, node->name); if (isDirectory) *numDirectoryEntries = *numDirectoryEntries + 1; } else { root = node; setFullPath(node, startMusicPath, ""); } } } fclose(file); free(nodes); return root; } int min(int a, int b, int c) { if (a <= b && a <= c) return a; if (b <= a && b <= c) return b; return c; } #ifdef __GNUC__ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wmaybe-uninitialized" #endif // Calculates the Levenshtein distance. // The Levenshtein distance between two strings is the minimum number of single-character edits // (insertions, deletions, or substitutions) required to change one string into the other. int levenshteinDistance(const char *s1, const char *s2) { int len1 = strlen(s1); int len2 = strlen(s2); int **d = (int **)malloc((len1 + 1) * sizeof(int *)); for (int i = 0; i <= len1; i++) { d[i] = (int *)malloc((len2 + 1) * sizeof(int)); for (int j = 0; j <= len2; j++) { d[i][j] = 0; } } for (int i = 0; i <= len1; i++) { d[i][0] = i; } for (int j = 0; j <= len2; j++) { d[0][j] = j; } for (int i = 1; i <= len1; i++) { for (int j = 1; j <= len2; j++) { int cost = (s1[i - 1] == s2[j - 1]) ? 0 : 1; d[i][j] = min(d[i - 1][j] + 1, // deletion d[i][j - 1] + 1, // insertion d[i - 1][j - 1] + cost); // substitution } } int distance = d[len1][len2]; for (int i = 0; i <= len1; i++) { free(d[i]); } free(d); return distance; } #ifdef __GNUC__ #pragma GCC diagnostic pop #endif // Returns a new string that is lowercase of str char *strLower(char *str) { char *lowerStr = strdup(str); for (char *p = lowerStr; *p; ++p) { *p = tolower(*p); } return lowerStr; } // Traverses the tree and applies fuzzy search on each node void fuzzySearchRecursive(FileSystemEntry *node, const char *searchTerm, int threshold, void (*callback)(FileSystemEntry *, int)) { if (node == NULL) { return; } // Convert search term, name, and fullPath to lowercase char *lowerSearchTerm = strLower((char *)searchTerm); char *lowerName = strLower(node->name); int nameDistance = levenshteinDistance(lowerName, lowerSearchTerm); // Partial matching with lowercase strings if (strstr(lowerName, lowerSearchTerm) != NULL) { callback(node, 0); } else if (nameDistance <= threshold) { callback(node, nameDistance); } // Free the allocated memory for lowercase strings free(lowerSearchTerm); free(lowerName); fuzzySearchRecursive(node->children, searchTerm, threshold, callback); fuzzySearchRecursive(node->next, searchTerm, threshold, callback); } FileSystemEntry *findCorrespondingEntry(FileSystemEntry *temp, const char *fullPath) { if (temp == NULL) return NULL; if (strcmp(temp->fullPath, fullPath) == 0) return temp; FileSystemEntry *found = findCorrespondingEntry(temp->children, fullPath); if (found != NULL) return found; return findCorrespondingEntry(temp->next, fullPath); } void copyIsEnqueued(FileSystemEntry *library, FileSystemEntry *temp) { if (library == NULL) return; if (library->isEnqueued) { FileSystemEntry *tempEntry = findCorrespondingEntry(temp, library->fullPath); if (tempEntry != NULL) { tempEntry->isEnqueued = library->isEnqueued; } } copyIsEnqueued(library->children, temp); copyIsEnqueued(library->next, temp); } kew-2.8.2/src/directorytree.h000066400000000000000000000024371467402032100161420ustar00rootroot00000000000000#ifndef DIRECTORYTREE_H #define DIRECTORYTREE_H #include #include #include #include #include #include #include #include #include "file.h" #include "utils.h" #ifndef FILE_SYSTEM_ENTRY #define FILE_SYSTEM_ENTRY typedef struct FileSystemEntry { int id; char *name; char *fullPath; int isDirectory; // 1 for directory, 0 for file int isEnqueued; int parentId; struct FileSystemEntry *parent; struct FileSystemEntry *children; struct FileSystemEntry *next; // For siblings (next node in the same directory) } FileSystemEntry; #endif #ifndef SLOWLOADING_CALLBACK #define SLOWLOADING_CALLBACK typedef void (*SlowloadingCallback)(void); #endif FileSystemEntry *createDirectoryTree(const char *startPath, int *numEntries); void freeTree(FileSystemEntry *root); void freeAndWriteTree(FileSystemEntry *root, const char *filename); FileSystemEntry *reconstructTreeFromFile(const char *filename, const char *startMusicPath, int *numDirectoryEntries); void fuzzySearchRecursive(FileSystemEntry *node, const char *searchTerm, int threshold, void (*callback)(FileSystemEntry *, int)); void copyIsEnqueued(FileSystemEntry *library, FileSystemEntry *temp); #endif kew-2.8.2/src/events.h000066400000000000000000000023111467402032100145510ustar00rootroot00000000000000#define MAX_SEQ_LEN 1024 // Maximum length of sequence buffer enum EventType { EVENT_NONE, EVENT_PLAY_PAUSE, EVENT_VOLUME_UP, EVENT_VOLUME_DOWN, EVENT_NEXT, EVENT_PREV, EVENT_QUIT, EVENT_TOGGLECOVERS, EVENT_TOGGLEREPEAT, EVENT_TOGGLEVISUALIZER, EVENT_TOGGLEBLOCKS, EVENT_ADDTOMAINPLAYLIST, EVENT_DELETEFROMMAINPLAYLIST, EVENT_EXPORTPLAYLIST, EVENT_UPDATELIBRARY, EVENT_SHUFFLE, EVENT_KEY_PRESS, EVENT_SHOWKEYBINDINGS, EVENT_SHOWPLAYLIST, EVENT_SHOWSEARCH, EVENT_GOTOSONG, EVENT_GOTOBEGINNINGOFPLAYLIST, EVENT_GOTOENDOFPLAYLIST, EVENT_TOGGLE_PROFILE_COLORS, EVENT_SCROLLNEXT, EVENT_SCROLLPREV, EVENT_SEEKBACK, EVENT_SEEKFORWARD, EVENT_SHOWLIBRARY, EVENT_SHOWTRACK, EVENT_NEXTPAGE, EVENT_PREVPAGE, EVENT_REMOVE, EVENT_SEARCH, EVENT_TABNEXT }; struct Event { enum EventType type; char key[MAX_SEQ_LEN]; // To store multi-byte characters }; typedef struct { char *seq; enum EventType eventType; } EventMapping; kew-2.8.2/src/file.c000066400000000000000000000301431467402032100141630ustar00rootroot00000000000000#ifndef _DEFAULT_SOURCE #define _DEFAULT_SOURCE #endif #include "file.h" /* file.c This file should contain only simple utility functions related to files and directories. They should work independently and be as decoupled from the rest of the application as possible. */ void getDirectoryFromPath(const char *path, char *directory) { char tempPath[strlen(path) + 1]; // tempPath is needed because the dirname function modifies the input string strcpy(tempPath, path); char *dir = dirname(tempPath); strcpy(directory, dir); if (directory[strlen(directory) - 1] != '/') { strcat(directory, "/"); } } int existsFile(const char *fname) { FILE *file; if ((file = fopen(fname, "r"))) { fclose(file); return 1; } return -1; } int isDirectory(const char *path) { DIR *dir = opendir(path); if (dir) { closedir(dir); return 1; } else { if (errno == ENOENT) { return -1; } return 0; } } // Traverse a directory tree and search for a given file or directory int walker(const char *startPath, const char *searching, char *result, const char *allowedExtensions, enum SearchType searchType, bool exactSearch) { DIR *d; struct dirent *dir; struct stat file_stat; char ext[6]; // +1 for null-terminator regex_t regex; int ret = regcomp(®ex, allowedExtensions, REG_EXTENDED); if (ret != 0) { return -1; } bool copyresult = false; if (startPath != NULL) { d = opendir(startPath); if (d == NULL) { fprintf(stderr, "Failed to open directory.\n"); return 0; } int chdirResult = chdir(startPath); if (chdirResult != 0) { fprintf(stderr, "Failed to change directory: %s\n", startPath); return 0; } } else { d = opendir("."); if (d == NULL) { fprintf(stderr, "Failed to open current directory.\n"); return 0; } } while ((dir = readdir(d))) { if (strcmp(dir->d_name, ".") == 0 || strcmp(dir->d_name, "..") == 0) { continue; } char entryPath[MAXPATHLEN]; char *currentDir = getcwd(NULL, 0); snprintf(entryPath, sizeof(entryPath), "%s/%s", currentDir, dir->d_name); free(currentDir); if (stat(entryPath, &file_stat) != 0) { continue; } if (S_ISDIR(file_stat.st_mode)) { if (((exactSearch && (strcasecmp(dir->d_name, searching) == 0)) || (!exactSearch && c_strcasestr(dir->d_name, searching) != NULL)) && (searchType != FileOnly) && (searchType != SearchPlayList)) { char *curDir = getcwd(NULL, 0); snprintf(result, MAXPATHLEN, "%s/%s", curDir, dir->d_name); free(curDir); copyresult = true; break; } else { if (chdir(dir->d_name) == -1) { fprintf(stderr, "Failed to change directory: %s\n", dir->d_name); continue; } if (walker(NULL, searching, result, allowedExtensions, searchType, exactSearch) == 0) { copyresult = true; break; } if (chdir("..") == -1) { fprintf(stderr, "Failed to change directory to parent.\n"); break; } } } else { if (searchType == DirOnly) { continue; } char *filename = dir->d_name; if (strlen(filename) <= 4) { continue; } extractExtension(filename, sizeof(ext) - 1, ext); if (match_regex(®ex, ext) != 0) { continue; } if ((exactSearch && (strcasecmp(dir->d_name, searching) == 0)) || (!exactSearch && c_strcasestr(dir->d_name, searching) != NULL)) { char *curDir = getcwd(NULL, 0); snprintf(result, MAXPATHLEN, "%s/%s", curDir, dir->d_name); copyresult = true; free(curDir); break; } } } closedir(d); regfree(®ex); return copyresult ? 0 : 1; } int expandPath(const char *inputPath, char *expandedPath) { if (inputPath[0] == '\0' || inputPath[0] == '\r') return -1; if (inputPath[0] == '~') // Check if inputPath starts with '~' { const char *homeDir; if (inputPath[1] == '/' || inputPath[1] == '\0') // Handle "~/" { homeDir = getenv("HOME"); if (homeDir == NULL) { return -1; // Unable to retrieve home directory } inputPath++; // Skip '~' character } else // Handle "~username/" { const char *username = inputPath + 1; const char *slash = strchr(username, '/'); if (slash == NULL) { struct passwd *pw = getpwnam(username); if (pw == NULL) { return -1; // Unable to retrieve user directory } homeDir = pw->pw_dir; inputPath = ""; // Empty path component after '~username' } else { size_t usernameLen = slash - username; struct passwd *pw = getpwuid(getuid()); if (pw == NULL) { return -1; // Unable to retrieve user directory } homeDir = pw->pw_dir; inputPath += usernameLen + 1; // Skip '~username/' component } } size_t homeDirLen = strlen(homeDir); size_t inputPathLen = strlen(inputPath); if (homeDirLen + inputPathLen >= MAXPATHLEN) { return -1; // Expanded path exceeds maximum length } strcpy(expandedPath, homeDir); strcat(expandedPath, inputPath); } else // Handle if path is not prefixed with '~' { if (realpath(inputPath, expandedPath) == NULL) { return -1; // Unable to expand the path } } return 0; // Path expansion successful } int createDirectory(const char *path) { struct stat st; // Check if directory already exists if (stat(path, &st) == 0) { if (S_ISDIR(st.st_mode)) return 0; // Directory already exists else return -1; // Path exists but is not a directory } // Directory does not exist, so create it if (mkdir(path, 0700) == 0) return 1; // Directory created successfully return -1; // Failed to create directory } int removeDirectory(const char *path) { struct stat st; // Check if path exists if (stat(path, &st) != 0) return -1; // Path does not exist // Check if it is a directory if (!S_ISDIR(st.st_mode)) return -1; // Path exists but is not a directory DIR *dir = opendir(path); if (dir == NULL) return -1; // Failed to open directory struct dirent *entry; char filePath[MAXPATHLEN]; // Remove all entries in the directory while ((entry = readdir(dir)) != NULL) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; // Skip current and parent directories snprintf(filePath, sizeof(filePath), "%s/%s", path, entry->d_name); if (entry->d_type == DT_DIR) removeDirectory(filePath); // Recursively remove subdirectories else remove(filePath); // Remove regular file } closedir(dir); // Remove the directory itself if (rmdir(path) == 0) return 0; // Directory removed successfully return -1; // Failed to remove directory } int deleteFile(const char *filePath) { if (remove(filePath) == 0) { return 0; } else { return -1; } } int isInTempDir(const char *path) { const char *tempDir = getenv("TMPDIR"); if (tempDir == NULL) { #ifdef __APPLE__ tempDir = "/tmp"; #else tempDir = "/tmp"; #endif } return (startsWith(path, tempDir)); } void deleteTempDir() { const char *tempDir = getenv("TMPDIR"); if (tempDir == NULL) { #ifdef __APPLE__ tempDir = "/tmp"; #else tempDir = "/tmp"; #endif } else { } char dirPath[MAXPATHLEN]; struct passwd *pw = getpwuid(getuid()); const char *username = pw->pw_name; snprintf(dirPath, MAXPATHLEN, "%s/kew/%s", tempDir, username); removeDirectory(dirPath); } bool checkFileBelowMaxSize(const char *filePath, int maxSize) { struct stat st; if (stat(filePath, &st) == 0) { return (st.st_size <= maxSize); } perror("stat failed"); return false; } void generateTempFilePath(char *filePath, const char *prefix, const char *suffix) { const char *tempDir = getenv("TMPDIR"); if (tempDir == NULL) { tempDir = "/tmp"; } char dirPath[MAXPATHLEN]; struct passwd *pw = getpwuid(getuid()); const char *username = pw->pw_name; snprintf(dirPath, MAXPATHLEN, "%s/kew", tempDir); createDirectory(dirPath); snprintf(dirPath, MAXPATHLEN, "%s/kew/%s", tempDir, username); createDirectory(dirPath); char randomString[7]; for (int i = 0; i < 6; ++i) { randomString[i] = 'a' + rand() % 26; } randomString[6] = '\0'; snprintf(filePath, MAXPATHLEN + 7, "%s/%s%.6s%s", dirPath, prefix, randomString, suffix); } kew-2.8.2/src/file.h000066400000000000000000000024761467402032100142000ustar00rootroot00000000000000#ifndef FILE_H #define FILE_H #include #include #include #include #include #include #include #include #include #include #include #include #include #define __USE_GNU #include #include "utils.h" #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif #ifndef AUDIO_EXTENSIONS #define AUDIO_EXTENSIONS "\\.(m4a|aac|mp3|ogg|flac|wav|opus)$" #endif enum SearchType { SearchAny = 0, DirOnly = 1, FileOnly = 2, SearchPlayList = 3, ReturnAllSongs = 4 }; void getDirectoryFromPath(const char *path, char *directory); int isDirectory(const char *path); /* Traverse a directory tree and search for a given file or directory */ int walker(const char *startPath, const char *searching, char *result, const char *allowedExtensions, enum SearchType searchType, bool exactSearch); int expandPath(const char *inputPath, char *expandedPath); int createDirectory(const char *path); int removeDirectory(const char *path); int deleteFile(const char *filePath); void generateTempFilePath(char *filePath, const char *prefix, const char *suffix); void deleteTempDir(void); int isInTempDir(const char *path); int existsFile(const char *fname); #endif kew-2.8.2/src/kew.c000066400000000000000000001360161467402032100140400ustar00rootroot00000000000000/* kew - a command-line music player Copyright (C) 2022 Ravachol http://github.com/ravachol/kew $$\ $$ | $$ | $$\ $$$$$$\ $$\ $$\ $$\ $$ | $$ |$$ __$$\ $$ | $$ | $$ | $$$$$$ / $$$$$$$$ |$$ | $$ | $$ | $$ _$$< $$ ____|$$ | $$ | $$ | $$ | \$$\ \$$$$$$$\ \$$$$$\$$$$ | \__| \__| \_______| \_____\____/ 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 2 of the License, or (at your option) any later version, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #ifndef _POSIX_C_SOURCE #define _POSIX_C_SOURCE 200809L #endif #ifndef __USE_POSIX #define __USE_POSIX #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "cache.h" #include "events.h" #include "file.h" #include "mpris.h" #include "player.h" #include "playerops.h" #include "playlist.h" #include "search_ui.h" #include "settings.h" #include "sound.h" #include "soundcommon.h" #include "songloader.h" #include "utils.h" #ifdef USE_LIBNOTIFY #include "libnotify/notify.h" #endif // #define DEBUG 1 #define MAX_TMP_SEQ_LEN 256 // Maximum length of temporary sequence buffer #define COOLDOWN_MS 500 #define COOLDOWN2_MS 100 FILE *logFile = NULL; struct winsize windowSize; static bool eventProcessed = false; char digitsPressed[MAX_SEQ_LEN]; int digitsPressedCount = 0; int maxDigitsPressedCount = 9; static unsigned int updateCounter = 0; bool gPressed = false; bool loadingAudioData = false; bool goingToSong = false; bool startFromTop = false; bool exactSearch = false; AppSettings settings; int fuzzySearchThreshold = 2; int lastNotifiedId = -1; bool songWasRemoved = false; bool noPlaylist = false; bool isCooldownElapsed(int milliSeconds) { struct timespec currentTime; clock_gettime(CLOCK_MONOTONIC, ¤tTime); double elapsedMilliseconds = (currentTime.tv_sec - lastInputTime.tv_sec) * 1000.0 + (currentTime.tv_nsec - lastInputTime.tv_nsec) / 1000000.0; return elapsedMilliseconds >= milliSeconds; } struct Event processInput() { struct Event event; event.type = EVENT_NONE; event.key[0] = '\0'; bool cooldownElapsed = false; bool cooldown2Elapsed = false; if (!isInputAvailable()) { flushSeek(); return event; } if (isCooldownElapsed(COOLDOWN_MS) && !eventProcessed) cooldownElapsed = true; if (isCooldownElapsed(COOLDOWN2_MS) && !eventProcessed) cooldown2Elapsed = true; int seqLength = 0; char seq[MAX_SEQ_LEN]; seq[0] = '\0'; // Set initial value int keyReleased = 0; // Find input while (isInputAvailable()) { char tmpSeq[MAX_TMP_SEQ_LEN]; seqLength = seqLength + readInputSequence(tmpSeq, sizeof(tmpSeq)); // Release most keys directly, seekbackward and seekforward can be read continuously if (seqLength <= 0 && strcmp(seq + 1, settings.seekBackward) != 0 && strcmp(seq + 1, settings.seekForward) != 0) { keyReleased = 1; break; } if (strlen(seq) + strlen(tmpSeq) >= MAX_SEQ_LEN) { break; } strcat(seq, tmpSeq); // This slows the continous reads down to not get a a too fast scrolling speed if (strcmp(seq + 1, settings.hardScrollUp) == 0 || strcmp(seq + 1, settings.hardScrollDown) == 0 || strcmp(seq + 1, settings.scrollUpAlt) == 0 || strcmp(seq + 1, settings.scrollDownAlt) == 0 || strcmp(seq + 1, settings.seekBackward) == 0 || strcmp(seq + 1, settings.seekForward) == 0 || strcmp(seq + 1, settings.hardNextPage) == 0 || strcmp(seq + 1, settings.hardPrevPage) == 0) { keyReleased = 0; readInputSequence(tmpSeq, sizeof(tmpSeq)); // dummy read to prevent scrolling after key released break; } keyReleased = 0; } if (keyReleased) return event; eventProcessed = true; event.type = EVENT_NONE; strncpy(event.key, seq, MAX_SEQ_LEN); if (appState.currentView == SEARCH_VIEW) { if (strcmp(event.key, "\x7F") == 0 || strcmp(event.key, "\x08") == 0) { removeFromSearchText(); chosenSearchResultRow = 0; fuzzySearch(getLibrary(), fuzzySearchThreshold); event.type = EVENT_SEARCH; } else if (((strlen(event.key) == 1 && event.key[0] != '\033' && event.key[0] != '\n' && event.key[0] != '\t' && event.key[0] != '\r') || strcmp(event.key, " ") == 0 || (unsigned char)event.key[0] >= 0xC0)) { addToSearchText(event.key); chosenSearchResultRow = 0; fuzzySearch(getLibrary(), fuzzySearchThreshold); event.type = EVENT_SEARCH; } } // Map keys to events EventMapping keyMappings[] = {{settings.scrollUpAlt, EVENT_SCROLLPREV}, {settings.scrollDownAlt, EVENT_SCROLLNEXT}, {settings.nextTrackAlt, EVENT_NEXT}, {settings.previousTrackAlt, EVENT_PREV}, {settings.volumeUp, EVENT_VOLUME_UP}, {settings.volumeUpAlt, EVENT_VOLUME_UP}, {settings.volumeDown, EVENT_VOLUME_DOWN}, {settings.togglePause, EVENT_PLAY_PAUSE}, {settings.quit, EVENT_QUIT}, {settings.hardQuit, EVENT_QUIT}, {settings.toggleShuffle, EVENT_SHUFFLE}, {settings.toggleVisualizer, EVENT_TOGGLEVISUALIZER}, {settings.toggleAscii, EVENT_TOGGLEBLOCKS}, {settings.switchNumberedSong, EVENT_GOTOSONG}, {settings.seekBackward, EVENT_SEEKBACK}, {settings.seekForward, EVENT_SEEKFORWARD}, {settings.toggleRepeat, EVENT_TOGGLEREPEAT}, {settings.savePlaylist, EVENT_EXPORTPLAYLIST}, {settings.toggleColorsDerivedFrom, EVENT_TOGGLE_PROFILE_COLORS}, {settings.addToMainPlaylist, EVENT_ADDTOMAINPLAYLIST}, {settings.updateLibrary, EVENT_UPDATELIBRARY}, {settings.hardPlayPause, EVENT_PLAY_PAUSE}, {settings.hardPrev, EVENT_PREV}, {settings.hardNext, EVENT_NEXT}, {settings.hardSwitchNumberedSong, EVENT_GOTOSONG}, {settings.hardScrollUp, EVENT_SCROLLPREV}, {settings.hardScrollDown, EVENT_SCROLLNEXT}, {settings.hardShowPlaylist, EVENT_SHOWPLAYLIST}, {settings.hardShowPlaylistAlt, EVENT_SHOWPLAYLIST}, {settings.showPlaylistAlt, EVENT_SHOWPLAYLIST}, {settings.hardShowKeys, EVENT_SHOWKEYBINDINGS}, {settings.hardShowKeysAlt, EVENT_SHOWKEYBINDINGS}, {settings.showKeysAlt, EVENT_SHOWKEYBINDINGS}, {settings.hardEndOfPlaylist, EVENT_GOTOENDOFPLAYLIST}, {settings.hardShowTrack, EVENT_SHOWTRACK}, {settings.hardShowTrackAlt, EVENT_SHOWTRACK}, {settings.showTrackAlt, EVENT_SHOWTRACK}, {settings.hardShowLibrary, EVENT_SHOWLIBRARY}, {settings.hardShowLibraryAlt, EVENT_SHOWLIBRARY}, {settings.showLibraryAlt, EVENT_SHOWLIBRARY}, {settings.hardShowSearch, EVENT_SHOWSEARCH}, {settings.hardShowSearchAlt, EVENT_SHOWSEARCH}, {settings.showSearchAlt, EVENT_SHOWSEARCH}, {settings.hardNextPage, EVENT_NEXTPAGE}, {settings.hardPrevPage, EVENT_PREVPAGE}, {settings.hardRemove, EVENT_REMOVE}, {settings.hardRemove2, EVENT_REMOVE}, {settings.tabNext, EVENT_TABNEXT}}; int numKeyMappings = sizeof(keyMappings) / sizeof(EventMapping); // Set event for pressed key for (int i = 0; i < numKeyMappings; i++) { if (keyMappings[i].seq[0] != '\0' && ((seq[0] == '\033' && strlen(seq) > 1 && strcmp(seq + 1, keyMappings[i].seq) == 0) || strcmp(seq, keyMappings[i].seq) == 0)) { if (event.type == EVENT_SEARCH && keyMappings[i].eventType != EVENT_GOTOSONG) { break; } event.type = keyMappings[i].eventType; break; } } // Handle gg if (event.key[0] == 'g' && event.type == EVENT_NONE) { if (gPressed) { event.type = EVENT_GOTOBEGINNINGOFPLAYLIST; gPressed = false; } else { gPressed = true; } } // Handle numbers if (isdigit(event.key[0])) { if (digitsPressedCount < maxDigitsPressedCount) digitsPressed[digitsPressedCount++] = event.key[0]; } else { // Handle multiple digits, sometimes mixed with other keys for (int i = 0; i < MAX_SEQ_LEN; i++) { if (isdigit(seq[i])) { if (digitsPressedCount < maxDigitsPressedCount) digitsPressed[digitsPressedCount++] = seq[i]; } else { if (seq[i] == '\0') break; if (seq[i] != settings.switchNumberedSong[0] && seq[i] != settings.hardSwitchNumberedSong[0] && seq[i] != settings.hardEndOfPlaylist[0]) { memset(digitsPressed, '\0', sizeof(digitsPressed)); digitsPressedCount = 0; break; } else if (seq[i] == settings.hardEndOfPlaylist[0]) { event.type = EVENT_GOTOENDOFPLAYLIST; break; } else { event.type = EVENT_GOTOSONG; break; } } } } // Handle song prev/next cooldown if (!cooldownElapsed && (event.type == EVENT_NEXT || event.type == EVENT_PREV)) event.type = EVENT_NONE; else if (event.type == EVENT_NEXT || event.type == EVENT_PREV) updateLastInputTime(); // Handle seek/remove cooldown if (!cooldown2Elapsed && (event.type == EVENT_REMOVE || event.type == EVENT_SEEKBACK || event.type == EVENT_SEEKFORWARD)) event.type = EVENT_NONE; else if (event.type == EVENT_REMOVE || event.type == EVENT_SEEKBACK || event.type == EVENT_SEEKFORWARD) updateLastInputTime(); // Forget Numbers if (event.type != EVENT_GOTOSONG && event.type != EVENT_GOTOENDOFPLAYLIST && event.type != EVENT_NONE) { memset(digitsPressed, '\0', sizeof(digitsPressed)); digitsPressedCount = 0; } // Forget g pressed if (event.key[0] != 'g') { gPressed = false; } return event; } void setEndOfListReached() { appState.currentView = LIBRARY_VIEW; loadedNextSong = false; audioData.endOfListReached = true; usingSongDataA = false; currentSong = NULL; audioData.currentFileIndex = 0; audioData.restart = true; loadingdata.loadA = true; emitMetadataChanged("", "", "", "", "/org/mpris/MediaPlayer2/TrackList/NoTrack", NULL, 0); emitPlaybackStoppedMpris(); pthread_mutex_lock(&dataSourceMutex); cleanupPlaybackDevice(); pthread_mutex_unlock(&dataSourceMutex); refresh = true; chosenRow = playlist.count - 1; } void notifyMPRISSwitch(SongData *currentSongData) { if (currentSongData == NULL) return; gint64 length = getLengthInMicroSec(currentSongData->duration); // update mpris emitMetadataChanged( currentSongData->metadata->title, currentSongData->metadata->artist, currentSongData->metadata->album, currentSongData->coverArtPath, currentSongData->trackId != NULL ? currentSongData->trackId : "", currentSong, length); } void notifySongSwitch(SongData *currentSongData) { if (currentSongData != NULL && currentSongData->hasErrors == 0 && currentSongData->metadata && strlen(currentSongData->metadata->title) > 0) { #ifdef USE_LIBNOTIFY displaySongNotification(currentSongData->metadata->artist, currentSongData->metadata->title, currentSongData->coverArtPath); #endif notifyMPRISSwitch(currentSongData); lastNotifiedId = currentSong->id; } } void determineSongAndNotify() { SongData *currentSongData = NULL; bool isDeleted = determineCurrentSongData(¤tSongData); if (lastNotifiedId != currentSong->id) { if (!isDeleted) notifySongSwitch(currentSongData); } } // Checks conditions for refreshing player bool shouldRefreshPlayer() { return !skipping && !isEOFReached() && !isImplSwitchReached(); } // Refreshes the player visually if conditions are met void refreshPlayer() { int mutexResult = pthread_mutex_trylock(&switchMutex); if (mutexResult != 0) { fprintf(stderr, "Failed to lock switch mutex.\n"); return; } if (doNotifyMPRISSwitched) { doNotifyMPRISSwitched = false; notifyMPRISSwitch(getCurrentSongData()); } if (shouldRefreshPlayer()) { printPlayer(getCurrentSongData(), elapsedSeconds, &settings); } pthread_mutex_unlock(&switchMutex); } void resetListAfterDequeuingPlayingSong() { if (lastPlayedId < 0) return; Node *node = findSelectedEntryById(&playlist, lastPlayedId); if (currentSong == NULL && node == NULL) { stopPlayback(); loadedNextSong = false; audioData.endOfListReached = true; audioData.restart = true; emitMetadataChanged("", "", "", "", "/org/mpris/MediaPlayer2/TrackList/NoTrack", NULL, 0); emitPlaybackStoppedMpris(); pthread_mutex_lock(&dataSourceMutex); cleanupPlaybackDevice(); pthread_mutex_unlock(&dataSourceMutex); refresh = true; switchAudioImplementation(); unloadSongA(); unloadSongB(); songWasRemoved = true; userData.currentSongData = NULL; audioData.currentFileIndex = 0; audioData.restart = true; waitingForNext = true; startFromTop = true; loadingdata.loadA = true; usingSongDataA = false; ma_data_source_uninit(&audioData); audioData.switchFiles = false; if (playlist.count == 0) songToStartFrom = NULL; } } void handleGoToSong() { if (goingToSong) return; goingToSong = true; if (appState.currentView == LIBRARY_VIEW) { if (audioData.restart) { Node *lastSong = findSelectedEntryById(&playlist, lastPlayedId); startFromTop = false; if (lastSong == NULL) { if (playlist.tail != NULL) lastPlayedId = playlist.tail->id; else { lastPlayedId = -1; startFromTop = true; } } } pthread_mutex_lock(&(playlist.mutex)); enqueueSongs(getCurrentLibEntry()); resetListAfterDequeuingPlayingSong(); pthread_mutex_unlock(&(playlist.mutex)); } else if (appState.currentView == SEARCH_VIEW) { pthread_mutex_lock(&(playlist.mutex)); setChosenDir(getCurrentSearchEntry()); enqueueSongs(getCurrentSearchEntry()); resetListAfterDequeuingPlayingSong(); pthread_mutex_unlock(&(playlist.mutex)); } else { if (digitsPressedCount == 0) { if (isPaused() && currentSong != NULL && chosenNodeId == currentSong->id) { togglePause(&totalPauseSeconds, &pauseSeconds, &pause_time); } else { loadedNextSong = true; playlistNeedsUpdate = false; nextSongNeedsRebuilding = false; unloadSongA(); unloadSongB(); usingSongDataA = false; audioData.currentFileIndex = 0; loadingdata.loadA = true; bool wasEndOfList = false; if (audioData.endOfListReached) wasEndOfList = true; skipToSong(chosenNodeId, true); if ((songWasRemoved && currentSong != NULL)) { usingSongDataA = !usingSongDataA; songWasRemoved = false; } if (wasEndOfList) usingSongDataA = true; audioData.endOfListReached = false; } } else { resetPlaylistDisplay = true; int songNumber = atoi(digitsPressed); memset(digitsPressed, '\0', sizeof(digitsPressed)); digitsPressedCount = 0; playlistNeedsUpdate = false; nextSongNeedsRebuilding = false; skipToNumberedSong(songNumber); } } goingToSong = false; } void gotoBeginningOfPlaylist() { digitsPressed[0] = 1; digitsPressed[1] = '\0'; digitsPressedCount = 1; handleGoToSong(); } void gotoEndOfPlaylist() { if (digitsPressedCount > 0) { handleGoToSong(); } else { skipToLastSong(); } } void handleInput() { struct Event event = processInput(); switch (event.type) { case EVENT_GOTOBEGINNINGOFPLAYLIST: gotoBeginningOfPlaylist(); break; case EVENT_GOTOENDOFPLAYLIST: gotoEndOfPlaylist(); break; case EVENT_GOTOSONG: handleGoToSong(); break; case EVENT_PLAY_PAUSE: togglePause(&totalPauseSeconds, &pauseSeconds, &pause_time); break; case EVENT_TOGGLEVISUALIZER: toggleVisualizer(&settings); break; case EVENT_TOGGLEREPEAT: toggleRepeat(); break; case EVENT_TOGGLEBLOCKS: toggleBlocks(&settings); break; case EVENT_SHUFFLE: toggleShuffle(); emitShuffleChanged(); break; case EVENT_TOGGLE_PROFILE_COLORS: toggleColors(&settings); break; case EVENT_QUIT: quit(); break; case EVENT_SCROLLNEXT: scrollNext(); break; case EVENT_SCROLLPREV: scrollPrev(); break; case EVENT_VOLUME_UP: adjustVolumePercent(5); emitVolumeChanged(); break; case EVENT_VOLUME_DOWN: adjustVolumePercent(-5); emitVolumeChanged(); break; case EVENT_NEXT: resetPlaylistDisplay = true; skipToNextSong(); break; case EVENT_PREV: resetPlaylistDisplay = true; skipToPrevSong(); break; case EVENT_SEEKBACK: seekBack(); break; case EVENT_SEEKFORWARD: seekForward(); break; case EVENT_ADDTOMAINPLAYLIST: addToSpecialPlaylist(); break; case EVENT_EXPORTPLAYLIST: savePlaylist(settings.path); break; case EVENT_UPDATELIBRARY: updateLibrary(settings.path); break; case EVENT_SHOWKEYBINDINGS: toggleShowKeyBindings(); break; case EVENT_SHOWPLAYLIST: toggleShowPlaylist(); break; case EVENT_SHOWSEARCH: toggleShowSearch(); break; case EVENT_SHOWLIBRARY: toggleShowLibrary(); break; case EVENT_NEXTPAGE: flipNextPage(); break; case EVENT_PREVPAGE: flipPrevPage(); break; case EVENT_REMOVE: handleRemove(); resetListAfterDequeuingPlayingSong(); break; case EVENT_SHOWTRACK: showTrack(); break; case EVENT_TABNEXT: tabNext(); break; default: fastForwarding = false; rewinding = false; break; } eventProcessed = false; } void resize() { alarm(1); // Timer while (resizeFlag) { resizeFlag = 0; c_sleep(100); } alarm(0); // Cancel timer printf("\033[1;1H"); clearScreen(); refresh = true; } void updatePlayer() { struct winsize ws; ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws); // check if window has changed size if (ws.ws_col != windowSize.ws_col || ws.ws_row != windowSize.ws_row) { resizeFlag = 1; windowSize = ws; } // resizeFlag can also be set by handleResize if (resizeFlag) resize(); else { refreshPlayer(); } } void loadAudioData() { loadingAudioData = true; if (audioData.restart == true) { if (playlist.head != NULL && (waitingForPlaylist || waitingForNext)) { songLoading = true; if (waitingForPlaylist) { currentSong = playlist.head; } else if (waitingForNext) { if (songToStartFrom != NULL) { // Make sure it still exists in the playlist findNodeInList(&playlist, songToStartFrom->id, ¤tSong); songToStartFrom = NULL; } else if (lastPlayedId >= 0) { currentSong = findSelectedEntryById(&playlist, lastPlayedId); if (currentSong != NULL && currentSong->next != NULL) currentSong = currentSong->next; } if (currentSong == NULL) { if (startFromTop) { currentSong = playlist.head; startFromTop = false; } else currentSong = playlist.tail; } } audioData.restart = false; waitingForPlaylist = false; waitingForNext = false; songWasRemoved = false; if (isShuffleEnabled()) reshufflePlaylist(); unloadSongA(); unloadSongB(); int res = loadFirst(currentSong); finishLoading(); if (res >= 0) { res = createAudioDevice(&userData); } if (res >= 0) { resumePlayback(); } else { setEndOfListReached(); } playlistNeedsUpdate = false; loadedNextSong = false; nextSong = NULL; refresh = true; clock_gettime(CLOCK_MONOTONIC, &start_time); } } else if (currentSong != NULL && (nextSongNeedsRebuilding || nextSong == NULL) && !songLoading) { loadNextSong(); determineSongAndNotify(); } loadingAudioData = false; } void tryLoadNext() { songHasErrors = false; clearingErrors = true; if (tryNextSong == NULL && currentSong != NULL) tryNextSong = currentSong->next; else if (tryNextSong != NULL) tryNextSong = tryNextSong->next; if (tryNextSong != NULL) { songLoading = true; loadingdata.loadA = !usingSongDataA; loadingdata.loadingFirstDecoder = false; loadSong(tryNextSong, &loadingdata); } else { clearingErrors = false; } } void prepareNextSong() { if (!skipOutOfOrder && !isRepeatEnabled()) { setCurrentSongToNext(); } else { skipOutOfOrder = false; } finishLoading(); resetTimeCount(); nextSong = NULL; refresh = true; if (!isRepeatEnabled() || currentSong == NULL) { unloadPreviousSong(); } if (currentSong == NULL) { if (quitAfterStopping) quit(); else setEndOfListReached(); } else { determineSongAndNotify(); } clock_gettime(CLOCK_MONOTONIC, &start_time); } void handleSkipFromStopped() { // If we don't do this the song gets loaded in the wrong slot if (skipFromStopped) { usingSongDataA = !usingSongDataA; skipOutOfOrder = false; skipFromStopped = false; } } gboolean mainloop_callback(gpointer data) { (void)data; calcElapsedTime(); handleInput(); updateCounter++; // Update every other time or if searching (search needs to update often to detect keypresses) if (updateCounter % 2 == 0 || appState.currentView == SEARCH_VIEW) { // Process GDBus events in the global_main_context while (g_main_context_pending(global_main_context)) { g_main_context_iteration(global_main_context, FALSE); } updatePlayer(); if (playlist.head != NULL) { if (loadingAudioData == false && (skipFromStopped || !loadedNextSong || nextSongNeedsRebuilding) && !audioData.endOfListReached) { // handleSkipFromStopped(); loadAudioData(); } if (songHasErrors) tryLoadNext(); if (isPlaybackDone()) { updateLastSongSwitchTime(); prepareNextSong(); if (!doQuit) switchAudioImplementation(); } } else { setEOFNotReached(); } if (doQuit) { g_main_loop_quit(main_loop); return FALSE; } } return TRUE; } static gboolean quitOnSignal(gpointer user_data) { doQuit = true; GMainLoop *loop = (GMainLoop *)user_data; g_main_loop_quit(loop); return G_SOURCE_REMOVE; // Remove the signal source } void initFirstPlay(Node *song) { updateLastInputTime(); updateLastSongSwitchTime(); userData.currentSongData = NULL; userData.songdataA = NULL; userData.songdataB = NULL; userData.songdataADeleted = true; userData.songdataBDeleted = true; int res = 0; if (song != NULL) { audioData.currentFileIndex = 0; loadingdata.loadA = true; res = loadFirst(song); if (res >= 0) { res = createAudioDevice(&userData); } if (res >= 0) { resumePlayback(); } if (res < 0) setEndOfListReached(); } if (song == NULL || res < 0) { song = NULL; waitingForPlaylist = true; } loadedNextSong = false; nextSong = NULL; refresh = true; clock_gettime(CLOCK_MONOTONIC, &start_time); main_loop = g_main_loop_new(NULL, FALSE); g_unix_signal_add(SIGINT, quitOnSignal, main_loop); g_unix_signal_add(SIGHUP, quitOnSignal, main_loop); if (song != NULL) emitStartPlayingMpris(); else emitPlaybackStoppedMpris(); g_timeout_add(50, mainloop_callback, NULL); g_main_loop_run(main_loop); g_main_loop_unref(main_loop); } void cleanupOnExit() { pthread_mutex_lock(&dataSourceMutex); resetDecoders(); resetVorbisDecoders(); resetOpusDecoders(); resetM4aDecoders(); if (isContextInitialized) { cleanupPlaybackDevice(); cleanupAudioContext(); } emitPlaybackStoppedMpris(); bool noMusicFound = false; if (library == NULL || library->children == NULL) { noMusicFound = true; } if (!userData.songdataADeleted) { userData.songdataADeleted = true; unloadSongData(&loadingdata.songdataA); } if (!userData.songdataBDeleted) { userData.songdataBDeleted = true; unloadSongData(&loadingdata.songdataB); } freeSearchResults(); cleanupMpris(); restoreTerminalMode(); enableInputBuffering(); setConfig(&settings); saveSpecialPlaylist(settings.path); freeAudioBuffer(); deleteCache(tempCache); deleteTempDir(); freeMainDirectoryTree(); deletePlaylist(&playlist); deletePlaylist(originalPlaylist); deletePlaylist(specialPlaylist); free(specialPlaylist); free(originalPlaylist); setDefaultTextColor(); pthread_mutex_destroy(&(loadingdata.mutex)); pthread_mutex_destroy(&(playlist.mutex)); pthread_mutex_destroy(&(switchMutex)); pthread_mutex_unlock(&dataSourceMutex); pthread_mutex_destroy(&(dataSourceMutex)); #ifdef USE_LIBNOTIFY notify_uninit(); #endif resetConsole(); showCursor(); fflush(stdout); if (noMusicFound) { printf("No Music found.\n"); printf("Please make sure the path is set correctly. \n"); printf("To set it type: kew path \"/path/to/Music\". \n"); } else if (noPlaylist) { printf("Music not found.\n"); } #ifdef DEBUG fclose(logFile); #endif if (freopen("/dev/stderr", "w", stderr) == NULL) { perror("freopen error"); } } void run() { if (originalPlaylist == NULL) { originalPlaylist = malloc(sizeof(PlayList)); *originalPlaylist = deepCopyPlayList(&playlist); } if (playlist.head == NULL) { appState.currentView = LIBRARY_VIEW; } initMpris(); currentSong = playlist.head; initFirstPlay(currentSong); clearScreen(); fflush(stdout); } void init() { disableInputBuffering(); srand(time(NULL)); initResize(); ioctl(STDOUT_FILENO, TIOCGWINSZ, &windowSize); enableScrolling(); setNonblockingMode(); tempCache = createCache(); c_strcpy(loadingdata.filePath, sizeof(loadingdata.filePath), ""); loadingdata.songdataA = NULL; loadingdata.songdataB = NULL; loadingdata.loadA = true; loadingdata.loadingFirstDecoder = true; audioData.restart = true; userData.songdataADeleted = true; userData.songdataBDeleted = true; initAudioBuffer(); initVisuals(); pthread_mutex_init(&dataSourceMutex, NULL); pthread_mutex_init(&switchMutex, NULL); pthread_mutex_init(&(loadingdata.mutex), NULL); pthread_mutex_init(&(playlist.mutex), NULL); nerdFontsEnabled = true; createLibrary(&settings); setlocale(LC_ALL, ""); fflush(stdout); #ifdef USE_LIBNOTIFY notify_init("kew"); #endif #ifdef DEBUG g_setenv("G_MESSAGES_DEBUG", "all", TRUE); logFile = freopen("error.log", "w", stderr); if (logFile == NULL) { fprintf(stdout, "Failed to redirect stderr to error.log\n"); } #else FILE *nullStream = freopen("/dev/null", "w", stderr); (void)nullStream; #endif } void openLibrary() { appState.currentView = LIBRARY_VIEW; init(); playlist.head = NULL; run(); } void playSpecialPlaylist() { if (specialPlaylist->count == 0) { printf("Couldn't find any songs in the special playlist. Add a song by pressing '.' while it's playing. \n"); exit(0); } playingMainPlaylist = true; init(); deepCopyPlayListOntoList(specialPlaylist, &playlist); shufflePlaylist(&playlist); run(); } void playAll() { init(); createPlayListFromFileSystemEntry(library, &playlist, MAX_FILES); if (playlist.count == 0) { exit(0); } shufflePlaylist(&playlist); run(); } void playAllAlbums() { init(); addShuffledAlbumsToPlayList(library, &playlist, MAX_FILES); if (playlist.count == 0) { exit(0); } run(); } void removeArgElement(char *argv[], int index, int *argc) { if (index < 0 || index >= *argc) { // Invalid index return; } // Shift elements after the index for (int i = index; i < *argc - 1; i++) { argv[i] = argv[i + 1]; } // Update the argument count (*argc)--; } void handleOptions(int *argc, char *argv[]) { const char *noUiOption = "--noui"; const char *noCoverOption = "--nocover"; const char *quitOnStop = "--quitonstop"; const char *quitOnStop2 = "-q"; const char *exactOption = "--exact"; const char *exactOption2 = "-e"; int idx = -1; for (int i = 0; i < *argc; i++) { if (c_strcasestr(argv[i], noUiOption)) { uiEnabled = false; idx = i; } } if (idx >= 0) removeArgElement(argv, idx, argc); idx = -1; for (int i = 0; i < *argc; i++) { if (c_strcasestr(argv[i], noCoverOption)) { coverEnabled = false; idx = i; } } if (idx >= 0) removeArgElement(argv, idx, argc); idx = -1; for (int i = 0; i < *argc; i++) { if (c_strcasestr(argv[i], quitOnStop) || c_strcasestr(argv[i], quitOnStop2)) { quitAfterStopping = true; idx = i; } } if (idx >= 0) removeArgElement(argv, idx, argc); idx = -1; for (int i = 0; i < *argc; i++) { if (c_strcasestr(argv[i], exactOption) || c_strcasestr(argv[i], exactOption2)) { exactSearch = true; idx = i; } } if (idx >= 0) removeArgElement(argv, idx, argc); } #define PIDFILE_TEMPLATE "/tmp/kew_%d.pid" // Template for user-specific PID file int isProcessRunning(pid_t pid) { char proc_path[64]; snprintf(proc_path, sizeof(proc_path), "/proc/%d", pid); struct stat statbuf; return (stat(proc_path, &statbuf) == 0); } // Ensures only a single instance of kew can run at a time for the current user. void exitIfAlreadyRunning() { char pidfile_path[256]; snprintf(pidfile_path, sizeof(pidfile_path), PIDFILE_TEMPLATE, getuid()); FILE *pidfile; pid_t pid; pidfile = fopen(pidfile_path, "r"); if (pidfile != NULL) { if (fscanf(pidfile, "%d", &pid) == 1) { fclose(pidfile); if (isProcessRunning(pid)) { fprintf(stderr, "An instance of kew is already running. Pid: %d.\n", pid); exit(EXIT_FAILURE); } else { unlink(pidfile_path); } } else { fclose(pidfile); unlink(pidfile_path); } } // Create a new PID file pidfile = fopen(pidfile_path, "w"); if (pidfile == NULL) { perror("Unable to create PID file"); exit(EXIT_FAILURE); } fprintf(pidfile, "%d\n", getpid()); fclose(pidfile); } int directoryExists(const char *path) { struct stat info; if (stat(path, &info) != 0) { return 0; } else if (S_ISDIR(info.st_mode)) { return 1; } return 0; } void setMusicPath() { char *user = getenv("USER"); // Fallback if USER is not set if (!user) { user = getlogin(); if (!user) { struct passwd *pw = getpwuid(getuid()); if (pw) { user = pw->pw_name; } else { printf("Error: Could not retrieve user information.\n"); printf("Please set a path to your music library. \n"); printf("To set it type: kew path \"/path/to/Music\". \n"); exit(0); } } } // Music folder names in different languages const char *musicFolderNames[] = { "Music", "Música", "Musique", "Musik", "Musica", "Muziek", "Музыка", "音乐", "音楽", "음악", "موسيقى", "संगीत", "Müzik", "Musikk", "Μουσική", "Muzyka", "Hudba", "Musiikki", "Zene", "Muzică", "เพลง", "מוזיקה"}; char path[MAXPATHLEN]; int found = 0; char choice = ' '; int result = -1; for (size_t i = 0; i < sizeof(musicFolderNames) / sizeof(musicFolderNames[0]); i++) { snprintf(path, sizeof(path), "/home/%s/%s", user, musicFolderNames[i]); if (directoryExists(path)) { found = 1; printf("Do you want to use %s as your music library folder?\n", path); printf("y = Yes\nn = Enter a path\n"); result = scanf(" %c", &choice); if (choice == 'y' || choice == 'Y') { strncpy(settings.path, path, sizeof(settings.path)); return; } else if (choice == 'n' || choice == 'N') { break; // Enter a custom path } else { printf("Invalid choice. Please try again.\n"); i--; } } } if (!found || (found && (choice == 'n' || choice == 'N'))) { printf("Please enter the path to your music library (/path/to/Music):\n"); result = scanf("%s", path); if (directoryExists(path)) { strncpy(settings.path, path, sizeof(settings.path)); } else { printf("The entered path does not exist.\n"); exit(1); } } if (result == -1) exit(1); } int main(int argc, char *argv[]) { exitIfAlreadyRunning(); if ((argc == 2 && ((strcmp(argv[1], "--help") == 0) || (strcmp(argv[1], "-h") == 0) || (strcmp(argv[1], "-?") == 0)))) { showHelp(); exit(0); } else if (argc == 2 && (strcmp(argv[1], "--version") == 0 || strcmp(argv[1], "-v") == 0)) { printAbout(NULL); exit(0); } getConfig(&settings); if (argc == 3 && (strcmp(argv[1], "path") == 0)) { c_strcpy(settings.path, sizeof(settings.path), argv[2]); setConfig(&settings); exit(0); } if (settings.path[0] == '\0') { setMusicPath(); } atexit(cleanupOnExit); handleOptions(&argc, argv); loadSpecialPlaylist(settings.path); if (argc == 1) { openLibrary(); } else if (argc == 2 && strcmp(argv[1], "all") == 0) { playAll(); } else if (argc == 2 && strcmp(argv[1], "albums") == 0) { playAllAlbums(); } else if (argc == 2 && strcmp(argv[1], ".") == 0) { playSpecialPlaylist(); } else if (argc >= 2) { init(); makePlaylist(argc, argv, exactSearch, settings.path); if (playlist.count == 0) { noPlaylist = true; exit(0); } run(); } return 0; } kew-2.8.2/src/m4a.h000066400000000000000000000622411467402032100137360ustar00rootroot00000000000000 /* This implements a data source that decodes m4a streams via FFmpeg This object can be plugged into any `ma_data_source_*()` API and can also be used as a custom decoding backend. See the custom_decoder example. You need to include this file after miniaudio.h. */ #ifndef m4a_h #define m4a_h #ifdef __cplusplus extern "C" { #endif #include #include #include #include #include #include #include #include #include typedef struct { ma_data_source_base ds; /* The m4a decoder can be used independently as a data source. */ ma_read_proc onRead; ma_seek_proc onSeek; ma_tell_proc onTell; void *pReadSeekTellUserData; ma_format format; FILE *mf; // FFmpeg related fields... AVCodecContext *codec_context; SwrContext *swr_ctx; AVFormatContext *format_context; ma_uint64 cursor; ma_uint32 sampleSize; int bitDepth; } m4a_decoder; MA_API ma_result m4a_decoder_init(ma_read_proc onRead, ma_seek_proc onSeek, ma_tell_proc onTell, void *pReadSeekTellUserData, const ma_decoding_backend_config *pConfig, const ma_allocation_callbacks *pAllocationCallbacks, m4a_decoder *pM4a); MA_API ma_result m4a_decoder_init_file(const char *pFilePath, const ma_decoding_backend_config *pConfig, const ma_allocation_callbacks *pAllocationCallbacks, m4a_decoder *pM4a); MA_API void m4a_decoder_uninit(m4a_decoder *pM4a, const ma_allocation_callbacks *pAllocationCallbacks); MA_API ma_result m4a_decoder_read_pcm_frames(m4a_decoder *pM4a, void *pFramesOut, ma_uint64 frameCount, ma_uint64 *pFramesRead); MA_API ma_result m4a_decoder_seek_to_pcm_frame(m4a_decoder *pM4a, ma_uint64 frameIndex); MA_API ma_result m4a_decoder_get_data_format(m4a_decoder *pM4a, ma_format *pFormat, ma_uint32 *pChannels, ma_uint32 *pSampleRate, ma_channel *pChannelMap, size_t channelMapCap); MA_API ma_result m4a_decoder_get_cursor_in_pcm_frames(m4a_decoder *pM4a, ma_uint64 *pCursor); MA_API ma_result m4a_decoder_get_length_in_pcm_frames(m4a_decoder *pM4a, ma_uint64 *pLength); #ifdef __cplusplus } #endif #if defined(MINIAUDIO_IMPLEMENTATION) || defined(MA_IMPLEMENTATION) #define MAX_CHANNELS 2 #define MAX_SAMPLES 4800 // Maximum expected frame size #define MAX_SAMPLE_SIZE 4 static uint8_t leftoverBuffer[MAX_SAMPLES * MAX_CHANNELS * MAX_SAMPLE_SIZE]; static ma_uint64 leftoverSampleCount = 0; extern ma_result m4a_decoder_ds_get_data_format(ma_data_source *pDataSource, ma_format *pFormat, ma_uint32 *pChannels, ma_uint32 *pSampleRate, ma_channel *pChannelMap, size_t channelMapCap); ma_result m4a_decoder_ds_read(ma_data_source *pDataSource, void *pFramesOut, ma_uint64 frameCount, ma_uint64 *pFramesRead) { return m4a_decoder_read_pcm_frames((m4a_decoder *)pDataSource, pFramesOut, frameCount, pFramesRead); } ma_result m4a_decoder_ds_seek(ma_data_source *pDataSource, ma_uint64 frameIndex) { return m4a_decoder_seek_to_pcm_frame((m4a_decoder *)pDataSource, frameIndex); } ma_result m4a_decoder_ds_get_data_format(ma_data_source *pDataSource, ma_format *pFormat, ma_uint32 *pChannels, ma_uint32 *pSampleRate, ma_channel *pChannelMap, size_t channelMapCap) { return m4a_decoder_get_data_format((m4a_decoder *)pDataSource, pFormat, pChannels, pSampleRate, pChannelMap, channelMapCap); } ma_result m4a_decoder_ds_get_cursor(ma_data_source *pDataSource, ma_uint64 *pCursor) { return m4a_decoder_get_cursor_in_pcm_frames((m4a_decoder *)pDataSource, pCursor); } ma_result m4a_decoder_ds_get_length(ma_data_source *pDataSource, ma_uint64 *pLength) { return m4a_decoder_get_length_in_pcm_frames((m4a_decoder *)pDataSource, pLength); } ma_data_source_vtable g_m4a_decoder_ds_vtable = { m4a_decoder_ds_read, m4a_decoder_ds_seek, m4a_decoder_ds_get_data_format, m4a_decoder_ds_get_cursor, m4a_decoder_ds_get_length, NULL, (ma_uint64)0}; // Custom FFmpeg read function wrapper int m4a_ffmpeg_read(void *opaque, uint8_t *buf, int buf_size) { m4a_decoder *pM4a = (m4a_decoder *)opaque; size_t bytesRead = 0; ma_result result = pM4a->onRead(pM4a->pReadSeekTellUserData, buf, buf_size, &bytesRead); if (result == MA_SUCCESS) { return bytesRead; } else { switch (result) { case MA_IO_ERROR: return AVERROR(EIO); case MA_INVALID_ARGS: return AVERROR(EINVAL); default: return AVERROR(EIO); } } } int64_t m4a_ffmpeg_seek(void *opaque, int64_t offset, int whence) { m4a_decoder *pM4a = (m4a_decoder *)opaque; if (whence == AVSEEK_SIZE) { if (pM4a->onTell) { ma_int64 fileSize; ma_result result = pM4a->onTell(pM4a->pReadSeekTellUserData, &fileSize); return result != MA_SUCCESS ? AVERROR(result) : fileSize; } else { return AVERROR(ENOSYS); } } ma_seek_origin origin = ma_seek_origin_start; switch (whence) { case SEEK_SET: origin = ma_seek_origin_start; break; case SEEK_CUR: origin = ma_seek_origin_current; break; case SEEK_END: origin = ma_seek_origin_end; break; default: return AVERROR(EINVAL); } if (pM4a->onSeek) { ma_result result = pM4a->onSeek(pM4a->pReadSeekTellUserData, offset, origin); if (result != MA_SUCCESS) { return AVERROR(result); } if (whence != SEEK_CUR || offset != 0) { // If the position really changed ma_int64 newPosition; result = pM4a->onTell(pM4a->pReadSeekTellUserData, &newPosition); return result != MA_SUCCESS ? AVERROR(result) : newPosition; } else { ma_int64 currentPosition; result = pM4a->onTell(pM4a->pReadSeekTellUserData, ¤tPosition); return result != MA_SUCCESS ? AVERROR(result) : currentPosition; } } else { return AVERROR(ENOSYS); } } static ma_result m4a_decoder_init_internal(const ma_decoding_backend_config *pConfig, m4a_decoder *pM4a) { if (pM4a == NULL) { return MA_INVALID_ARGS; } MA_ZERO_OBJECT(pM4a); pM4a->format = ma_format_f32; if (pConfig != NULL && (pConfig->preferredFormat == ma_format_f32 || pConfig->preferredFormat == ma_format_s16)) { pM4a->format = pConfig->preferredFormat; } ma_data_source_config dataSourceConfig = ma_data_source_config_init(); dataSourceConfig.vtable = &g_m4a_decoder_ds_vtable; ma_result result = ma_data_source_init(&dataSourceConfig, &pM4a->ds); if (result != MA_SUCCESS) { return result; } return MA_SUCCESS; } ma_format ffmpeg_to_mini_al_format(enum AVSampleFormat ffmpeg_sample_fmt) { switch (ffmpeg_sample_fmt) { case AV_SAMPLE_FMT_FLTP: case AV_SAMPLE_FMT_FLT: return ma_format_f32; case AV_SAMPLE_FMT_S16: case AV_SAMPLE_FMT_S16P: return ma_format_s16; default: return ma_format_unknown; } } // Note: This isn't used by kew and is untested MA_API ma_result m4a_decoder_init( ma_read_proc onRead, ma_seek_proc onSeek, ma_tell_proc onTell, void *pReadSeekTellUserData, const ma_decoding_backend_config *pConfig, const ma_allocation_callbacks *pAllocationCallbacks, m4a_decoder *pM4a) { (void)onTell; (void)pAllocationCallbacks; if (pM4a == NULL || onRead == NULL || onSeek == NULL) { return MA_INVALID_ARGS; } ma_result result = m4a_decoder_init_internal(pConfig, pM4a); if (result != MA_SUCCESS) { return result; } unsigned char *avio_ctx_buffer = NULL; size_t avio_ctx_buffer_size = 4096; AVIOContext *avio_ctx = avio_alloc_context( avio_ctx_buffer, avio_ctx_buffer_size, 0, pReadSeekTellUserData, m4a_ffmpeg_read, NULL, m4a_ffmpeg_seek); if (avio_ctx == NULL) { return MA_OUT_OF_MEMORY; } avio_ctx_buffer = (unsigned char *)av_malloc(avio_ctx_buffer_size); if (avio_ctx_buffer == NULL) { avio_context_free(&avio_ctx); return MA_OUT_OF_MEMORY; } // Initialize FFmpeg's AVFormatContext with the custom I/O context pM4a->format_context = avformat_alloc_context(); if (pM4a->format_context == NULL) { av_free(avio_ctx_buffer); avio_context_free(&avio_ctx); return MA_OUT_OF_MEMORY; } pM4a->format_context->pb = avio_ctx; return MA_SUCCESS; } MA_API ma_result m4a_decoder_init_file(const char *pFilePath, const ma_decoding_backend_config *pConfig, const ma_allocation_callbacks *pAllocationCallbacks, m4a_decoder *pM4a) { (void)pAllocationCallbacks; if (pFilePath == NULL || pM4a == NULL) { return MA_INVALID_ARGS; } ma_result result = m4a_decoder_init_internal(pConfig, pM4a); if (result != MA_SUCCESS) { return result; } // Initialize libavformat and libavcodec AVFormatContext *format_context = NULL; if (avformat_open_input(&format_context, pFilePath, NULL, NULL) != 0) { return MA_INVALID_FILE; } if (avformat_find_stream_info(format_context, NULL) < 0) { avformat_close_input(&format_context); return MA_ERROR; } int stream_index; #if (LIBAVFORMAT_VERSION_MAJOR > 58) const AVCodec *decoder = NULL; stream_index = av_find_best_stream(format_context, AVMEDIA_TYPE_AUDIO, -1, -1, &decoder, 0); #else AVCodec *decoder = NULL; stream_index = av_find_best_stream(format_context, AVMEDIA_TYPE_AUDIO, -1, -1, &decoder, 0); #endif if (stream_index < 0) { avformat_close_input(&format_context); return MA_ERROR; } AVStream *audio_stream = format_context->streams[stream_index]; AVCodecContext *codec_context = avcodec_alloc_context3(decoder); if (!codec_context) { avformat_close_input(&format_context); return MA_OUT_OF_MEMORY; } if (avcodec_parameters_to_context(codec_context, audio_stream->codecpar) < 0) { avcodec_free_context(&codec_context); avformat_close_input(&format_context); return MA_ERROR; } if (avcodec_open2(codec_context, decoder, NULL) < 0) { avcodec_free_context(&codec_context); avformat_close_input(&format_context); return MA_ERROR; } pM4a->codec_context = codec_context; switch (pM4a->codec_context->sample_fmt) { case AV_SAMPLE_FMT_S16: case AV_SAMPLE_FMT_S16P: pM4a->sampleSize = sizeof(int16_t); // 16-bit samples break; case AV_SAMPLE_FMT_FLTP: case AV_SAMPLE_FMT_FLT: pM4a->sampleSize = sizeof(float); // 32-bit float samples break; default: pM4a->sampleSize = 0; break; } pM4a->format_context = format_context; pM4a->mf = NULL; pM4a->format = ffmpeg_to_mini_al_format(pM4a->codec_context->sample_fmt); return MA_SUCCESS; } MA_API void m4a_decoder_uninit(m4a_decoder *pM4a, const ma_allocation_callbacks *pAllocationCallbacks) { if (pM4a == NULL) { return; } (void)pAllocationCallbacks; if (pM4a->swr_ctx != NULL) { swr_free(&pM4a->swr_ctx); } if (pM4a->codec_context != NULL) { avcodec_free_context(&pM4a->codec_context); } if (pM4a->format_context != NULL) { avformat_close_input(&pM4a->format_context); } if (pM4a->mf != NULL) { fclose(pM4a->mf); pM4a->mf = NULL; } ma_data_source_uninit(&pM4a->ds); } ma_result m4a_decoder_read_pcm_frames(m4a_decoder *pM4a, void *pFramesOut, ma_uint64 frameCount, ma_uint64 *pFramesRead) { if (pM4a == NULL || pM4a->onRead == NULL || pM4a->onSeek == NULL || pM4a->sampleSize == 0 || pFramesOut == NULL || frameCount == 0) { return MA_INVALID_ARGS; } ma_result result = MA_SUCCESS; ma_format format; ma_uint32 channels; ma_uint32 sampleRate; m4a_decoder_get_data_format(pM4a, &format, &channels, &sampleRate, NULL, 0); // only two channels supported for now if (channels > 2) { return MA_ERROR; } int stream_index = av_find_best_stream(pM4a->format_context, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0); if (stream_index < 0) { return MA_ERROR; } AVFrame *frame = av_frame_alloc(); if (!frame) { return MA_ERROR; } AVPacket packet; ma_uint64 totalFramesProcessed = 0; if (leftoverSampleCount > 0) { int leftoverToProcess = (leftoverSampleCount < frameCount) ? leftoverSampleCount : frameCount; int leftoverBytes = leftoverToProcess * channels * pM4a->sampleSize; memcpy(pFramesOut, leftoverBuffer, leftoverBytes); totalFramesProcessed += leftoverToProcess; int bytesToShift = (leftoverSampleCount - leftoverToProcess) * channels * pM4a->sampleSize; uint8_t *shiftSource = leftoverBuffer + leftoverToProcess * channels * pM4a->sampleSize; memmove(leftoverBuffer, shiftSource, bytesToShift); leftoverSampleCount -= leftoverToProcess; } while (totalFramesProcessed < frameCount) { if (av_read_frame(pM4a->format_context, &packet) < 0) { // Error in reading frame or EOF result = MA_AT_END; break; } if (packet.stream_index == stream_index) { if (avcodec_send_packet(pM4a->codec_context, &packet) == 0) { while (totalFramesProcessed < frameCount && avcodec_receive_frame(pM4a->codec_context, frame) == 0) { int samplesToProcess = (frame->nb_samples < (int)(frameCount - totalFramesProcessed)) ? frame->nb_samples : (int)(frameCount - totalFramesProcessed); int outputBufferLen = samplesToProcess * channels * pM4a->sampleSize; uint8_t *output_buffer = (uint8_t *)malloc(outputBufferLen); if (!output_buffer) { av_frame_free(&frame); return MA_ERROR; } for (int i = 0; i < samplesToProcess; i++) { for (ma_uint32 c = 0; c < channels; c++) { int byteOffset = (i * channels + c) * pM4a->sampleSize; if (frame->extended_data == NULL || frame->extended_data[c] == NULL) { continue; } if (pM4a->format == ma_format_s16) { if (pM4a->codec_context->sample_fmt != AV_SAMPLE_FMT_S16) { continue; } int16_t sample = ((int16_t *)frame->extended_data[c])[i]; memcpy(output_buffer + byteOffset, &sample, sizeof(int16_t)); } else { float sample = ((float *)frame->extended_data[c])[i]; memcpy(output_buffer + byteOffset, &sample, sizeof(float)); } } } int current_frame_buffer_size = samplesToProcess * channels * pM4a->sampleSize; memcpy((uint8_t *)pFramesOut + totalFramesProcessed * channels * pM4a->sampleSize, output_buffer, current_frame_buffer_size); totalFramesProcessed += samplesToProcess; // Check if there are leftovers if (samplesToProcess < frame->nb_samples) { int remainingSamples = frame->nb_samples - samplesToProcess; for (int i = samplesToProcess; i < frame->nb_samples; i++) { for (ma_uint32 c = 0; c < channels; c++) { int byteOffset = ((i - samplesToProcess) * channels + c) * pM4a->sampleSize; if (frame->extended_data[c] == NULL) { return MA_ERROR; } memcpy(leftoverBuffer + byteOffset, (uint8_t *)frame->extended_data[c] + i * pM4a->sampleSize, pM4a->sampleSize); } } leftoverSampleCount = remainingSamples; } free(output_buffer); } } } av_packet_unref(&packet); } av_frame_free(&frame); pM4a->cursor += totalFramesProcessed; if (pFramesRead != NULL) { *pFramesRead = totalFramesProcessed; } return result; } MA_API ma_result m4a_decoder_seek_to_pcm_frame(m4a_decoder *pM4a, ma_uint64 frameIndex) { if (pM4a == NULL || pM4a->codec_context == NULL || pM4a->format_context == NULL) { return MA_INVALID_ARGS; } AVStream *stream = NULL; for (unsigned int i = 0; i < pM4a->format_context->nb_streams; i++) { if (pM4a->format_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { stream = pM4a->format_context->streams[i]; break; } } if (stream == NULL) { return MA_ERROR; } // Convert frame index to the stream's time base. int64_t timestamp = av_rescale_q(frameIndex, (AVRational){1, pM4a->codec_context->sample_rate}, stream->time_base); if (av_seek_frame(pM4a->format_context, stream->index, timestamp, AVSEEK_FLAG_BACKWARD) < 0) { return MA_ERROR; } // After seeking, we must clear the codec's internal buffer. avcodec_flush_buffers(pM4a->codec_context); return MA_SUCCESS; } MA_API ma_result m4a_decoder_get_data_format( m4a_decoder *pM4a, ma_format *pFormat, ma_uint32 *pChannels, ma_uint32 *pSampleRate, ma_channel *pChannelMap, size_t channelMapCap) { if (pFormat != NULL) { *pFormat = ma_format_unknown; } if (pChannels != NULL) { *pChannels = 0; } if (pSampleRate != NULL) { *pSampleRate = 0; } if (pChannelMap != NULL) { MA_ZERO_MEMORY(pChannelMap, sizeof(*pChannelMap) * channelMapCap); } if (pM4a == NULL || pM4a->codec_context == NULL) { return MA_INVALID_OPERATION; } if (pFormat != NULL) { *pFormat = ffmpeg_to_mini_al_format(pM4a->codec_context->sample_fmt); } if (pChannels != NULL) { for (unsigned int i = 0; i < pM4a->format_context->nb_streams; i++) { if (pM4a->format_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { #if (LIBAVCODEC_VERSION_MAJOR > 59) || ((LIBAVCODEC_VERSION_MAJOR == 59) && (LIBAVCODEC_VERSION_MINOR > 24)) *pChannels = pM4a->format_context->streams[i]->codecpar->ch_layout.nb_channels; #else *pChannels = pM4a->format_context->streams[i]->codecpar->channels; #endif } } } if (pSampleRate != NULL) { *pSampleRate = pM4a->codec_context->sample_rate; } if (pChannelMap != NULL) { ma_channel_map_init_standard(ma_standard_channel_map_microsoft, pChannelMap, channelMapCap, *pChannels); } return MA_SUCCESS; } MA_API ma_result m4a_decoder_get_cursor_in_pcm_frames(m4a_decoder *pM4a, ma_uint64 *pCursor) { if (pCursor == NULL) { return MA_INVALID_ARGS; } *pCursor = 0; /* Safety. */ if (pM4a == NULL || pM4a->format_context == NULL || pM4a->codec_context == NULL) { return MA_INVALID_ARGS; } *pCursor = pM4a->cursor; return MA_SUCCESS; } // Note: This returns an approximation MA_API ma_result m4a_decoder_get_length_in_pcm_frames(m4a_decoder *pM4a, ma_uint64 *pLength) { if (pLength == NULL) { return MA_INVALID_ARGS; } *pLength = 0; // Safety. if (pM4a == NULL || pM4a->format_context == NULL || pM4a->codec_context == NULL) { return MA_INVALID_ARGS; } AVStream *audio_stream = NULL; for (unsigned int i = 0; i < pM4a->format_context->nb_streams; i++) { if (pM4a->format_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { audio_stream = pM4a->format_context->streams[i]; break; } } if (audio_stream == NULL) { return MA_ERROR; } // Use duration and time base to calculate total number of frames if (audio_stream->duration != AV_NOPTS_VALUE) { int64_t duration_ts = audio_stream->duration; AVRational time_base = audio_stream->time_base; AVRational target_time_base = {1, pM4a->codec_context->sample_rate}; *pLength = av_rescale_q(duration_ts, time_base, target_time_base); return MA_SUCCESS; } return MA_ERROR; } #endif #endif kew-2.8.2/src/mpris.c000066400000000000000000001346541467402032100144120ustar00rootroot00000000000000#include "mpris.h" /* mpris.c Functions related to mpris implementation. */ GMainContext *global_main_context = NULL; GMainLoop *main_loop; guint registration_id; guint player_registration_id; guint bus_name_id; static const gchar *LoopStatus = "None"; static gdouble Rate = 1.0; static gdouble Volume = 0.5; static gdouble MinimumRate = 1.0; static gdouble MaximumRate = 1.0; static gboolean CanGoNext = TRUE; static gboolean CanGoPrevious = TRUE; static gboolean CanPlay = TRUE; static gboolean CanPause = TRUE; static gboolean CanSeek = FALSE; static gboolean CanControl = TRUE; void updatePlaybackStatus(const gchar *status) { GVariant *status_variant = g_variant_new_string(status); g_dbus_connection_emit_signal(connection, NULL, "/org/mpris/MediaPlayer2", "org.mpris.MediaPlayer2.Player", "PlaybackStatus", g_variant_new("(s)", status_variant), NULL); g_variant_unref(status_variant); } const gchar *introspection_xml = "\n" "\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "\n"; static const gchar *identity = "kew"; static const gchar *desktopIconName = ""; // Without file extension static const gchar *desktopEntry = ""; // The name of your .desktop file static void handle_raise(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)method_name; (void)parameters; (void)invocation; (void)user_data; } static void handle_quit(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)method_name; (void)parameters; (void)invocation; (void)user_data; quit(); } static gboolean get_identity(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; *value = g_variant_new_string(identity); return TRUE; } static gboolean get_desktop_entry(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; *value = g_variant_new_string(desktopEntry); return TRUE; } static gboolean get_desktop_icon_name(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; *value = g_variant_new_string(desktopIconName); return TRUE; } static void handle_next(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)method_name; (void)parameters; (void)user_data; skipToNextSong(); g_dbus_method_invocation_return_value(invocation, NULL); } static void handle_previous(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)method_name; (void)parameters; (void)user_data; skipToPrevSong(); g_dbus_method_invocation_return_value(invocation, NULL); } static void handle_pause(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)method_name; (void)parameters; (void)invocation; (void)user_data; playbackPause(&pause_time); } static void handle_play_pause(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)method_name; (void)parameters; (void)user_data; togglePause(&totalPauseSeconds, &pauseSeconds, &pause_time); g_dbus_method_invocation_return_value(invocation, NULL); } static void handle_stop(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)method_name; (void)parameters; (void)user_data; if (!isStopped()) stop(); g_dbus_method_invocation_return_value(invocation, NULL); } static void handle_play(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)method_name; (void)parameters; (void)invocation; (void)user_data; playbackPlay(&totalPauseSeconds, &pauseSeconds); } static void handle_seek(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)method_name; (void)user_data; gint64 offset; g_variant_get(parameters, "(x)", &offset); gboolean success = seekPosition(offset); if (success) { g_dbus_method_invocation_return_value(invocation, NULL); } else { g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "Failed to seek to position"); } } static void handle_set_position(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)method_name; (void)user_data; const gchar *track_id; gint64 new_position; // - "o" is an object path (or track identifier) // - "x" is a 64-bit integer representing the position g_variant_get(parameters, "(&ox)", &track_id, &new_position); gboolean success = setPosition(new_position); if (success) { // If setting the position was successful, return success with no additional value g_dbus_method_invocation_return_value(invocation, NULL); } else { // If setting the position failed, return an error g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "Failed to set position for track %s", track_id); } } static void handle_method_call(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { if (g_strcmp0(method_name, "PlayPause") == 0) { handle_play_pause(connection, sender, object_path, interface_name, method_name, parameters, invocation, user_data); } else if (g_strcmp0(method_name, "Next") == 0) { handle_next(connection, sender, object_path, interface_name, method_name, parameters, invocation, user_data); } else if (g_strcmp0(method_name, "Previous") == 0) { handle_previous(connection, sender, object_path, interface_name, method_name, parameters, invocation, user_data); } else if (g_strcmp0(method_name, "Pause") == 0) { handle_pause(connection, sender, object_path, interface_name, method_name, parameters, invocation, user_data); } else if (g_strcmp0(method_name, "Stop") == 0) { handle_stop(connection, sender, object_path, interface_name, method_name, parameters, invocation, user_data); } else if (g_strcmp0(method_name, "Play") == 0) { handle_play(connection, sender, object_path, interface_name, method_name, parameters, invocation, user_data); } else if (g_strcmp0(method_name, "Seek") == 0) { handle_seek(connection, sender, object_path, interface_name, method_name, parameters, invocation, user_data); } else if (g_strcmp0(method_name, "SetPosition") == 0) { handle_set_position(connection, sender, object_path, interface_name, method_name, parameters, invocation, user_data); } else if (g_strcmp0(method_name, "Raise") == 0) { handle_raise(connection, sender, object_path, interface_name, method_name, parameters, invocation, user_data); } else if (g_strcmp0(method_name, "Quit") == 0) { handle_quit(connection, sender, object_path, interface_name, method_name, parameters, invocation, user_data); } else { g_dbus_method_invocation_return_dbus_error(invocation, "org.freedesktop.DBus.Error.UnknownMethod", "No such method"); } } static void on_bus_name_acquired(GDBusConnection *connection, const gchar *name, gpointer user_data) { (void)connection; (void)name; (void)user_data; } static void on_bus_name_lost(GDBusConnection *connection, const gchar *name, gpointer user_data) { (void)connection; (void)name; (void)user_data; } static gboolean get_playback_status(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; const gchar *status = "Stopped"; if (isPaused()) { status = "Paused"; } else if (currentSong == NULL || isStopped()) { status = "Stopped"; } else { status = "Playing"; } *value = g_variant_new_string(status); return TRUE; } static gboolean get_loop_status(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; *value = g_variant_new_string(LoopStatus); return TRUE; } static gboolean get_rate(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; *value = g_variant_new_double(Rate); return TRUE; } static gboolean get_shuffle(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; *value = g_variant_new_boolean(isShuffleEnabled() ? TRUE : FALSE); return TRUE; } static gboolean get_metadata(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; SongData *currentSongData = getCurrentSongData(); GVariantBuilder metadata_builder; g_variant_builder_init(&metadata_builder, G_VARIANT_TYPE_DICTIONARY); if (currentSong != NULL && currentSongData != NULL && currentSongData->metadata != NULL) { g_variant_builder_add(&metadata_builder, "{sv}", "xesam:title", g_variant_new_string(currentSongData->metadata->title)); // Build list of strings for artist const gchar *artistList[2]; if (currentSongData->metadata->artist[0] != '\0') { artistList[0] = currentSongData->metadata->artist; artistList[1] = NULL; } else { artistList[0] = ""; artistList[1] = NULL; } gchar *coverArtUrl = g_strdup_printf("file://%s", currentSongData->coverArtPath); g_variant_builder_add(&metadata_builder, "{sv}", "xesam:artist", g_variant_new_strv(artistList, -1)); g_variant_builder_add(&metadata_builder, "{sv}", "xesam:album", g_variant_new_string(currentSongData->metadata->album)); g_variant_builder_add(&metadata_builder, "{sv}", "xesam:contentCreated", g_variant_new_string(currentSongData->metadata->date)); g_variant_builder_add(&metadata_builder, "{sv}", "mpris:artUrl", g_variant_new_string(coverArtUrl)); g_variant_builder_add(&metadata_builder, "{sv}", "mpris:trackid", g_variant_new_object_path(currentSongData->trackId)); gint64 length = llround(currentSongData->duration * G_USEC_PER_SEC); g_variant_builder_add(&metadata_builder, "{sv}", "mpris:length", g_variant_new_int64(length)); g_free(coverArtUrl); } else { g_variant_builder_add(&metadata_builder, "{sv}", "xesam:title", g_variant_new_string("")); g_variant_builder_add(&metadata_builder, "{sv}", "xesam:artist", g_variant_new_strv((const gchar *[]){"", NULL}, -1)); g_variant_builder_add(&metadata_builder, "{sv}", "xesam:album", g_variant_new_string("")); g_variant_builder_add(&metadata_builder, "{sv}", "xesam:contentCreated", g_variant_new_string("")); g_variant_builder_add(&metadata_builder, "{sv}", "mpris:artUrl", g_variant_new_string("")); g_variant_builder_add(&metadata_builder, "{sv}", "mpris:trackid", g_variant_new_object_path("/org/mpris/MediaPlayer2/TrackList/NoTrack")); gint64 placeholderLength = 0; g_variant_builder_add(&metadata_builder, "{sv}", "mpris:length", g_variant_new_int64(placeholderLength)); } GVariant *metadata_variant = g_variant_builder_end(&metadata_builder); *value = g_variant_ref_sink(metadata_variant); return TRUE; } static gboolean get_volume(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; Volume = (gdouble)getCurrentVolume(); if (Volume >= 1) Volume = Volume / 100; *value = g_variant_new_double(Volume); return TRUE; } static gboolean get_position(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; // Convert elapsedSeconds from milliseconds to microseconds gint64 positionMicroseconds = llround(elapsedSeconds * G_USEC_PER_SEC); *value = g_variant_new_int64(positionMicroseconds); return TRUE; } static gboolean get_minimum_rate(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; *value = g_variant_new_double(MinimumRate); return TRUE; } static gboolean get_maximum_rate(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; *value = g_variant_new_double(MaximumRate); return TRUE; } static gboolean get_can_go_next(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; CanGoNext = (currentSong == NULL || currentSong->next != NULL) ? TRUE : FALSE; *value = g_variant_new_boolean(CanGoNext); return TRUE; } static gboolean get_can_go_previous(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; CanGoPrevious = (currentSong == NULL || currentSong->prev != NULL) ? TRUE : FALSE; *value = g_variant_new_boolean(CanGoPrevious); return TRUE; } static gboolean get_can_play(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; if (currentSong == NULL) CanPlay = FALSE; else CanPlay = TRUE; *value = g_variant_new_boolean(CanPlay); return TRUE; } static gboolean get_can_pause(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; if (currentSong == NULL) CanPause = FALSE; else CanPause = TRUE; *value = g_variant_new_boolean(CanPause); return TRUE; } static gboolean get_can_seek(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; *value = g_variant_new_boolean(CanSeek); return TRUE; } static gboolean get_can_control(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; *value = g_variant_new_boolean(CanControl); return TRUE; } static GVariant *get_property_callback(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GError **error, gpointer user_data) { GVariant *value = NULL; if (g_strcmp0(property_name, "PlaybackStatus") == 0) { get_playback_status(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "LoopStatus") == 0) { get_loop_status(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "Rate") == 0) { get_rate(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "Shuffle") == 0) { get_shuffle(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "Metadata") == 0) { get_metadata(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "Volume") == 0) { get_volume(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "Position") == 0) { get_position(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "MinimumRate") == 0) { get_minimum_rate(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "MaximumRate") == 0) { get_maximum_rate(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "CanGoNext") == 0) { get_can_go_next(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "CanGoPrevious") == 0) { get_can_go_previous(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "CanPlay") == 0) { get_can_play(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "CanPause") == 0) { get_can_pause(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "CanSeek") == 0) { get_can_seek(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "CanControl") == 0) { get_can_control(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "DesktopIconName") == 0) { get_desktop_icon_name(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "DesktopEntry") == 0) { get_desktop_entry(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "Identity") == 0) { get_identity(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "Unknown property"); } // Check if value is NULL and set an error if needed if (value == NULL && error == NULL) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "Property value is NULL"); } return value; } static gboolean set_property_callback(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant *value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)user_data; if (g_strcmp0(interface_name, "org.mpris.MediaPlayer2.Player") == 0) { if (g_strcmp0(property_name, "PlaybackStatus") == 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "Setting PlaybackStatus property not supported"); return FALSE; } else if (g_strcmp0(property_name, "Volume") == 0) { double new_volume; g_variant_get(value, "d", &new_volume); if (new_volume > 1.0) new_volume = 1.0; if (new_volume < 0.0) new_volume = 0.0; new_volume *= 100; setVolume((int)new_volume); return TRUE; } else if (g_strcmp0(property_name, "LoopStatus") == 0) { toggleRepeat(); return TRUE; } else if (g_strcmp0(property_name, "Shuffle") == 0) { toggleShuffle(); return TRUE; } else if (g_strcmp0(property_name, "Position") == 0) { gint64 new_position; g_variant_get(value, "x", &new_position); return setPosition(new_position); } else { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "Setting property not supported"); return FALSE; } } else { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "Unknown interface"); return FALSE; } } // MPRIS MediaPlayer2 interface vtable static const GDBusInterfaceVTable media_player_interface_vtable = { .method_call = handle_method_call, // We're using individual method handlers .get_property = get_property_callback, // Handle the property getters individually .set_property = set_property_callback, .padding = { handle_raise, handle_quit}}; // MPRIS Player interface vtable static const GDBusInterfaceVTable player_interface_vtable = { .method_call = handle_method_call, // We're using individual method handlers .get_property = get_property_callback, // Handle the property getters individually .set_property = set_property_callback, .padding = { handle_next, handle_previous, handle_pause, handle_play_pause, handle_stop, handle_play, handle_seek, handle_set_position}}; void emitPlaybackStoppedMpris() { if (connection) { g_dbus_connection_call(connection, NULL, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Set", g_variant_new("(ssv)", "org.mpris.MediaPlayer2.Player", "PlaybackStatus", g_variant_new_string("Stopped")), G_VARIANT_TYPE("(v)"), G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); } } void cleanupMpris() { if (registration_id > 0) { g_dbus_connection_unregister_object(connection, registration_id); registration_id = -1; } if (player_registration_id > 0) { g_dbus_connection_unregister_object(connection, player_registration_id); player_registration_id = -1; } if (bus_name_id > 0) { g_bus_unown_name(bus_name_id); bus_name_id = -1; } if (connection != NULL) { g_object_unref(connection); connection = NULL; } if (global_main_context != NULL) { g_main_context_unref(global_main_context); global_main_context = NULL; } #ifdef USE_LIBNOTIFY if (previous_notification != NULL) { g_object_unref(previous_notification); previous_notification = NULL; } #endif } void initMpris() { if (global_main_context == NULL) { global_main_context = g_main_context_new(); } GDBusNodeInfo *introspection_data = g_dbus_node_info_new_for_xml(introspection_xml, NULL); connection = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, NULL); if (!connection) { g_dbus_node_info_unref(introspection_data); g_printerr("Failed to connect to D-Bus\n"); exit(0); } const char *app_name = "org.mpris.MediaPlayer2.kew"; GError *error = NULL; bus_name_id = g_bus_own_name_on_connection(connection, app_name, G_BUS_NAME_OWNER_FLAGS_NONE, on_bus_name_acquired, on_bus_name_lost, NULL, NULL); if (bus_name_id == 0) { printf("Failed to own D-Bus name: %s\n", app_name); exit(0); } registration_id = g_dbus_connection_register_object( connection, "/org/mpris/MediaPlayer2", introspection_data->interfaces[0], &media_player_interface_vtable, NULL, NULL, &error); if (!registration_id) { g_dbus_node_info_unref(introspection_data); g_printerr("Failed to register media player object: %s\n", error->message); g_error_free(error); exit(0); } player_registration_id = g_dbus_connection_register_object( connection, "/org/mpris/MediaPlayer2", introspection_data->interfaces[1], &player_interface_vtable, NULL, NULL, &error); if (!player_registration_id) { g_dbus_node_info_unref(introspection_data); g_printerr("Failed to register media player object: %s\n", error->message); g_error_free(error); exit(0); } g_dbus_node_info_unref(introspection_data); } void emitStartPlayingMpris() { GVariant *parameters = g_variant_new("(s)", "Playing"); g_dbus_connection_emit_signal(connection, NULL, "/org/mpris/MediaPlayer2", "org.mpris.MediaPlayer2.Player", "PlaybackStatusChanged", parameters, NULL); } gchar *sanitizeTitle(const gchar *title) { gchar *sanitized = g_strdup(title); // Replace underscores with hyphens, otherwise some widgets have a problem g_strdelimit(sanitized, "_", '-'); // duplicate string otherwise widgets have a problem with certain strings for some reason gchar *sanitized_dup = g_strdup_printf("%s", sanitized); g_free(sanitized); return sanitized_dup; } static guint64 last_emit_time = 0; void emit_properties_changed(GDBusConnection *connection, const gchar *property_name, GVariant *new_value) { GVariantBuilder changed_properties_builder; if (connection == NULL || property_name == NULL || new_value == NULL) return; // Initialize the builder for changed properties g_variant_builder_init(&changed_properties_builder, G_VARIANT_TYPE("a{sv}")); g_variant_builder_add(&changed_properties_builder, "{sv}", property_name, new_value); GError *error = NULL; gboolean result = g_dbus_connection_emit_signal(connection, NULL, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "PropertiesChanged", g_variant_new("(sa{sv}as)", "org.mpris.MediaPlayer2.Player", &changed_properties_builder, NULL), &error); if (!result) { g_critical("Failed to emit PropertiesChanged signal: %s", error->message); g_error_free(error); } else { g_debug("PropertiesChanged signal emitted successfully."); } g_variant_builder_clear(&changed_properties_builder); } void emitVolumeChanged() { gdouble newVolume = (gdouble)getCurrentVolume() / 100; if (newVolume > 1.0) return; // Emit the PropertiesChanged signal for the volume property GVariant *volume_variant = g_variant_new_double(newVolume); emit_properties_changed(connection, "Volume", volume_variant); } void emitShuffleChanged() { gboolean shuffleEnabled = isShuffleEnabled(); // Emit the PropertiesChanged signal for the volume property GVariant *volume_variant = g_variant_new_boolean(shuffleEnabled); emit_properties_changed(connection, "Shuffle", volume_variant); } void emitMetadataChanged(const gchar *title, const gchar *artist, const gchar *album, const gchar *coverArtPath, const gchar *trackId, Node *currentSong, gint64 length) { guint64 current_time = g_get_monotonic_time(); if (current_time - last_emit_time < 500000) // 0.5 seconds { g_debug("Debounced signal emission."); return; } last_emit_time = current_time; if (!title || !album || !trackId) { g_warning("Invalid metadata: title, album, or trackId is NULL."); return; } gchar *coverArtUrl = NULL; gchar *sanitizedTitle = sanitizeTitle(title); g_debug("Starting to build metadata."); GVariantBuilder metadata_builder; g_variant_builder_init(&metadata_builder, G_VARIANT_TYPE_DICTIONARY); g_variant_builder_add(&metadata_builder, "{sv}", "xesam:title", g_variant_new_string(sanitizedTitle)); g_free(sanitizedTitle); const gchar *artistList[2]; if (artist) { artistList[0] = artist; artistList[1] = NULL; } else { artistList[0] = ""; artistList[1] = NULL; } g_variant_builder_add(&metadata_builder, "{sv}", "xesam:artist", g_variant_new_strv(artistList, -1)); g_variant_builder_add(&metadata_builder, "{sv}", "xesam:album", g_variant_new_string(album)); if (coverArtPath && *coverArtPath != '\0') { coverArtUrl = g_strdup_printf("file://%s", coverArtPath); g_variant_builder_add(&metadata_builder, "{sv}", "mpris:artUrl", g_variant_new_string(coverArtUrl)); g_debug("Cover art URL added: %s", coverArtUrl); g_free(coverArtUrl); } g_variant_builder_add(&metadata_builder, "{sv}", "mpris:trackid", g_variant_new_object_path(trackId)); g_variant_builder_add(&metadata_builder, "{sv}", "mpris:length", g_variant_new_int64(length)); GVariant *metadata_variant = g_variant_builder_end(&metadata_builder); if (!metadata_variant) { g_warning("Failed to end metadata GVariantBuilder."); return; } g_debug("Metadata built successfully."); GVariantBuilder changed_properties_builder; g_variant_builder_init(&changed_properties_builder, G_VARIANT_TYPE("a{sv}")); g_variant_builder_add(&changed_properties_builder, "{sv}", "Metadata", metadata_variant); g_variant_builder_add(&changed_properties_builder, "{sv}", "CanGoPrevious", g_variant_new_boolean((currentSong != NULL && currentSong->prev != NULL))); g_variant_builder_add(&changed_properties_builder, "{sv}", "CanGoNext", g_variant_new_boolean((currentSong != NULL && currentSong->next != NULL))); CanSeek = true; if (currentSong != NULL && endsWith(currentSong->song.filePath, "ogg")) { CanSeek = false; } g_variant_builder_add(&changed_properties_builder, "{sv}", "CanSeek", g_variant_new_boolean(CanSeek)); g_debug("PropertiesChanged signal is ready to be emitted."); GError *error = NULL; gboolean result = g_dbus_connection_emit_signal(connection, NULL, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "PropertiesChanged", g_variant_new("(sa{sv}as)", "org.mpris.MediaPlayer2.Player", &changed_properties_builder, NULL), &error); if (!result) { g_critical("Failed to emit PropertiesChanged signal: %s", error->message); g_error_free(error); } else { g_debug("PropertiesChanged signal emitted successfully."); } g_variant_builder_clear(&changed_properties_builder); g_variant_builder_clear(&metadata_builder); } kew-2.8.2/src/mpris.h000066400000000000000000000014241467402032100144030ustar00rootroot00000000000000#ifndef MPRIS_H #define MPRIS_H #include #include #include "playerops.h" #include "playlist.h" #include "sound.h" #include "soundcommon.h" extern GDBusConnection *connection; extern GMainContext *global_main_context; extern GMainLoop *main_loop; void initMpris(void); void emitStringPropertyChanged(const gchar *propertyName, const gchar *newValue); void emitBooleanPropertyChanged(const gchar *propertyName, gboolean newValue); void emitVolumeChanged(); void emitShuffleChanged(); void emitMetadataChanged(const gchar *title, const gchar *artist, const gchar *album, const gchar *coverArtPath, const gchar *trackId, Node *currentSong, gint64 length); void emitStartPlayingMpris(void); void emitPlaybackStoppedMpris(void); void cleanupMpris(void); #endif kew-2.8.2/src/player.c000066400000000000000000001242511467402032100145440ustar00rootroot00000000000000#include "player.h" /* player.c Functions related to printing the player to the screen. */ #ifndef PIXELDATA_STRUCT #define PIXELDATA_STRUCT #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif typedef struct { unsigned char r; unsigned char g; unsigned char b; } PixelData; #endif const char VERSION[] = "2.8.2"; const int ABSOLUTE_MIN_WIDTH = 68; bool visualizerEnabled = true; bool coverEnabled = true; bool hideLogo = false; bool hideHelp = false; bool quitAfterStopping = false; bool coverAnsi = false; bool metaDataEnabled = true; bool timeEnabled = true; bool drewCover = true; bool uiEnabled = true; bool showList = true; bool resetPlaylistDisplay = true; bool fastForwarding = false; bool rewinding = false; bool nerdFontsEnabled = true; int numProgressBars = 35; int elapsedBars = 0; int chosenRow = 0; int chosenSong = 0; int aboutHeight = 8; int visualizerHeight = 5; int minWidth = ABSOLUTE_MIN_WIDTH; int minHeight = 2; int maxWidth = 0; int coverRow = 0; int preferredWidth = 0; int preferredHeight = 0; int textWidth = 0; int indent = 0; char *tagsPath; double totalDurationSeconds = 0.0; PixelData lastRowColor = {90, 90, 90}; TagSettings metadata = {}; double pauseSeconds = 0.0; double totalPauseSeconds = 0.0; double seekAccumulatedSeconds = 0.0; int maxListSize = 0; int maxSearchListSize = 0; int numDirectoryTreeEntries = 0; int numTopLevelSongs = 0; int startLibIter = 0; int startSearchIter = 0; int maxLibListSize = 0; int chosenLibRow = 0; int chosenSearchResultRow = 0; bool allowChooseSongs = false; FileSystemEntry *currentEntry = NULL; FileSystemEntry *chosenDir = NULL; int libIter = 0; int libSongIter = 0; int libTopLevelSongIter = 0; int chosenNodeId = 0; int cacheLibrary = -1; const char LIBRARY_FILE[] = "kewlibrary"; FileSystemEntry *library = NULL; bool hasNerdFonts() { return (printf("\uf28b") >= 0); // nerd fonts } int calcMetadataHeight() { int term_w, term_h; getTermSize(&term_w, &term_h); if (metadata.title[0] != '\0') { size_t titleLength = strlen(metadata.title); int titleHeight = (int)ceil((float)titleLength / term_w); size_t artistLength = strlen(metadata.artist); int artistHeight = (int)ceil((float)artistLength / term_w); size_t albumLength = strlen(metadata.album); int albumHeight = (int)ceil((float)albumLength / term_w); int yearHeight = 1; return titleHeight + artistHeight + albumHeight + yearHeight; } else { return 4; } } int calcIdealImgSize(int *width, int *height, const int visualizerHeight, const int metatagHeight) { float aspectRatio = calcAspectRatio(); int term_w, term_h; getTermSize(&term_w, &term_h); int timeDisplayHeight = 1; int heightMargin = 4; int minHeight = visualizerHeight + metatagHeight + timeDisplayHeight + heightMargin; int minBorderWidth = 0; *height = term_h - minHeight; *width = ceil(*height * aspectRatio); if (*width > term_w) { *width = term_w - minBorderWidth; *height = floor(*width / aspectRatio); } return 0; } void calcPreferredSize() { minHeight = 2 + (visualizerEnabled ? visualizerHeight : 0); calcIdealImgSize(&preferredWidth, &preferredHeight, (visualizerEnabled ? visualizerHeight : 0), calcMetadataHeight()); } void printHelp() { printf(" kew - a command-line music player.\n"); printf("\n"); printf(" \033[1;4mUsage:\033[0m kew path \"path to music library\"\n"); printf(" (Saves the music library path. Use this the first time. Ie: kew path \"/home/joe/Music/\")\n"); printf(" kew (no argument, opens library)\n"); printf(" kew all (loads all your songs up to 10 000)\n"); printf(" kew albums (plays all albums up to 2000 randomly one after the other)"); printf(" kew \n"); printf(" kew --help, -? or -h\n"); printf(" kew --version or -v\n"); printf(" kew dir (Sometimes it's necessary to specify it's a directory you want)\n"); printf(" kew song \n"); printf(" kew list \n"); printf(" kew shuffle (random and rand works too)\n"); printf(" kew artistA:artistB (plays artistA and artistB shuffled)\n"); printf(" kew . (plays kew.m3u file)\n"); printf("\n"); printf(" \033[1;4mExample:\033[0m kew moon\n"); printf(" (Plays the first song or directory it finds that has the word moon, ie moonlight sonata)\n"); printf("\n"); printf(" kew returns the first directory or file whose name partially matches the string you provide.\n\n"); printf(" Use quotes when providing strings with single quotes in them (') or vice versa.\n"); printf(" Use ←, → or h, l to play the next or previous track in the playlist.\n"); printf(" Use + (or =), - to adjust volume.\n"); printf(" Use a, d to seek in a song.\n"); printf(" Press space or p to pause.\n"); printf(" Press u to update the library.\n"); printf(" Press F2 to display playlist.\n"); printf(" Press F3 to display music library.\n"); printf(" Press F4 to display song info.\n"); printf(" Press F5 to search.\n"); printf(" Press F6 to display key bindings.\n"); printf(" Press . to add the currently playing song to kew.m3u.\n"); printf(" Press Esc to quit.\n"); printf("\n"); } int printLogo(SongData *songData) { if (useProfileColors) setTextColor(mainColor); else setColor(); int height = 0; int logoWidth = 0; if (!hideLogo) { printBlankSpaces(indent); printf(" __\n"); printBlankSpaces(indent); printf("| |--.-----.--.--.--.\n"); printBlankSpaces(indent); printf("| <| -__| | | |\n"); printBlankSpaces(indent); printf("|__|__|_____|________|"); logoWidth = 22; height += 3; } else { printf("\n"); height += 1; } if (songData != NULL && songData->metadata != NULL) { int term_w, term_h; getTermSize(&term_w, &term_h); char title[MAXPATHLEN] = {0}; if (hideLogo && songData->metadata->artist[0] != '\0') { printBlankSpaces(indent); snprintf(title, MAXPATHLEN, "%s - %s", songData->metadata->artist, songData->metadata->title); } else { strncpy(title, songData->metadata->title, MAXPATHLEN - 1); title[MAXPATHLEN - 1] = '\0'; } shortenString(title, term_w - indent - indent - logoWidth - 4); if (useProfileColors) setTextColor(titleColor); printf(" %s\n\n", title); height += 2; } else { printf("\n\n"); height += 2; } return height; } int getYear(const char *dateString) { int year; if (sscanf(dateString, "%d", &year) != 1) { return -1; } return year; } int displayCover(FIBITMAP *cover, const char *coverArtPath, int height, bool ansii) { if (!ansii) { printSquareBitmapCentered(cover, height); } else { int width = height * 2; output_ascii(coverArtPath, height, width); } printf("\n"); return 0; } void printCover(SongData *songdata) { clearRestOfScreen(); minWidth = ABSOLUTE_MIN_WIDTH + indent; if (songdata->cover != NULL && coverEnabled) { clearScreen(); displayCover(songdata->cover, songdata->coverArtPath, preferredHeight, coverAnsi); drewCover = true; } else { clearRestOfScreen(); for (int i = 0; i < preferredHeight - 1; i++) { printf("\n"); } drewCover = false; } } void printWithDelay(const char *text, int delay, int maxWidth) { int length = strlen(text); int max = (maxWidth > length) ? length : maxWidth; for (int i = 0; i <= max; i++) { printf("\r "); printBlankSpaces(indent); for (int j = 0; j < i; j++) { printf("%c", text[j]); } printf("█"); fflush(stdout); c_sleep(delay); } c_sleep(delay * 20); printf("\r"); printf("\033[K"); printBlankSpaces(indent); printf("\033[1K %.*s", maxWidth, text); printf("\n"); fflush(stdout); } void printBasicMetadata(TagSettings const *metadata) { int term_w, term_h; getTermSize(&term_w, &term_h); maxWidth = textWidth; // term_w - 3 - (indent * 2); printf("\n"); setColor(); int rows = 1; if (strlen(metadata->artist) > 0) { printBlankSpaces(indent); printf(" %.*s\n", maxWidth, metadata->artist); rows++; } if (strlen(metadata->album) > 0) { printBlankSpaces(indent); printf(" %.*s\n", maxWidth, metadata->album); rows++; } if (strlen(metadata->date) > 0) { printBlankSpaces(indent); int year = getYear(metadata->date); if (year == -1) printf(" %s\n", metadata->date); else printf(" %d\n", year); rows++; } cursorJump(rows); if (strlen(metadata->title) > 0) { PixelData pixel = increaseLuminosity(color, 20); if (pixel.r == 255 && pixel.g == 255 && pixel.b == 255) { PixelData gray; gray.r = defaultColor; gray.g = defaultColor; gray.b = defaultColor; printf("\033[1;38;2;%03u;%03u;%03um", gray.r, gray.g, gray.b); } else { printf("\033[1;38;2;%03u;%03u;%03um", pixel.r, pixel.g, pixel.b); } if (useProfileColors) printf("\e[1m\e[39m"); printWithDelay(metadata->title, 9, maxWidth - 2); } cursorJumpDown(rows - 1); } int calcElapsedBars(double elapsedSeconds, double duration, int numProgressBars) { if (elapsedSeconds == 0) return 0; return (int)((elapsedSeconds / duration) * numProgressBars); } void printProgress(double elapsed_seconds, double total_seconds) { int progressWidth = 39; int term_w, term_h; getTermSize(&term_w, &term_h); if (term_w < progressWidth) return; // Save the current cursor position printf("\033[s"); int elapsed_hours = (int)(elapsed_seconds / 3600); int elapsed_minutes = (int)(((int)elapsed_seconds / 60) % 60); int elapsed_seconds_remainder = (int)elapsed_seconds % 60; int total_hours = (int)(total_seconds / 3600); int total_minutes = (int)(((int)total_seconds / 60) % 60); int total_seconds_remainder = (int)total_seconds % 60; int progress_percentage = (int)((elapsed_seconds / total_seconds) * 100); int vol = getCurrentVolume(); // Clear the current line printf("\r\033[K"); printBlankSpaces(indent); printf(" %02d:%02d:%02d / %02d:%02d:%02d (%d%%) Vol:%d%%", elapsed_hours, elapsed_minutes, elapsed_seconds_remainder, total_hours, total_minutes, total_seconds_remainder, progress_percentage, vol); // Restore the cursor position printf("\033[u"); } void printMetadata(TagSettings const *metadata) { if (!metaDataEnabled || appState.currentView == LIBRARY_VIEW || appState.currentView == PLAYLIST_VIEW || appState.currentView == SEARCH_VIEW) return; c_sleep(100); setColor(); printBasicMetadata(metadata); } void printTime(double elapsedSeconds) { if (!timeEnabled || appState.currentView == LIBRARY_VIEW || appState.currentView == PLAYLIST_VIEW || appState.currentView == SEARCH_VIEW) return; setColor(); int term_w, term_h; getTermSize(&term_w, &term_h); printBlankSpaces(indent); if (term_h > minHeight) printProgress(elapsedSeconds, duration); } int getRandomNumber(int min, int max) { return min + rand() % (max - min + 1); } void printGlimmeringText(char *text, char *nerdFontText, PixelData color) { int textLength = strlen(text); int brightIndex = 0; PixelData vbright = increaseLuminosity(color, 120); PixelData bright = increaseLuminosity(color, 60); printBlankSpaces(indent); while (brightIndex < textLength) { for (int i = 0; i < textLength; i++) { if (i == brightIndex) { setTextColorRGB(vbright.r, vbright.g, vbright.b); printf("%c", text[i]); } else if (i == brightIndex - 1 || i == brightIndex + 1) { setTextColorRGB(bright.r, bright.g, bright.b); printf("%c", text[i]); } else { setTextColorRGB(color.r, color.g, color.b); printf("%c", text[i]); } fflush(stdout); c_usleep(50); } printf("%s", nerdFontText); fflush(stdout); c_usleep(50); brightIndex++; printf("\r"); printBlankSpaces(indent); } } void printLastRow() { int term_w, term_h; getTermSize(&term_w, &term_h); if (term_w < minWidth) return; setTextColorRGB(lastRowColor.r, lastRowColor.g, lastRowColor.b); char text[100] = " [F2 Playlist|F3 Library|F4 Track|F5 Search|F6 Help|Esc Quit]"; char nerdFontText[100] = ""; printf("\r"); if (nerdFontsEnabled) { if (isPaused()) { char pauseText[] = " \uf04c"; strcat(nerdFontText, pauseText); } if (isRepeatEnabled()) { char repeatText[] = " \uf01e"; strcat(nerdFontText, repeatText); } if (isShuffleEnabled()) { char shuffleText[] = " \uf074"; strcat(nerdFontText, shuffleText); } if (fastForwarding) { char forwardText[] = " \uf04e"; strcat(nerdFontText, forwardText); } if (rewinding) { char rewindText[] = " \uf04a"; strcat(nerdFontText, rewindText); } } else { if (isRepeatEnabled()) { char repeatText[] = " R"; strcat(text, repeatText); } if (isShuffleEnabled()) { char shuffleText[] = " S"; strcat(text, shuffleText); } } printf("\033[K"); // clear the line int randomNumber = getRandomNumber(1, 808); if (randomNumber == 808) printGlimmeringText(text, nerdFontText, lastRowColor); else { printBlankSpaces(indent); printf("%s", text); printf("%s", nerdFontText); } } int printAbout(SongData *songdata) { clearScreen(); int numRows = printLogo(songdata); setDefaultTextColor(); printBlankSpaces(indent); printf(" kew version: %s\n\n", VERSION); numRows += 2; return numRows; } int showKeyBindings(SongData *songdata, AppSettings *settings) { int numPrintedRows = 0; int term_w, term_h; getTermSize(&term_w, &term_h); numPrintedRows += printAbout(songdata); setDefaultTextColor(); printBlankSpaces(indent); printf(" - Switch tracks with ←, → or %s, %s keys.\n", settings->previousTrackAlt, settings->nextTrackAlt); printBlankSpaces(indent); printf(" - Volume is adjusted with %s (or %s) and %s.\n", settings->volumeUp, settings->volumeUpAlt, settings->volumeDown); printBlankSpaces(indent); printf(" - Press F2 for Playlist View:\n"); printBlankSpaces(indent); printf(" Use ↑, ↓ or %s, %s keys to scroll through the playlist.\n", settings->scrollUpAlt, settings->scrollDownAlt); printBlankSpaces(indent); printf(" Press Enter to play.\n"); printBlankSpaces(indent); printf(" - Press F3 for Library View:\n"); printBlankSpaces(indent); printf(" Use ↑, ↓ or %s, %s keys to scroll through the library.\n", settings->scrollUpAlt, settings->scrollDownAlt); printBlankSpaces(indent); printf(" Press Enter to add/remove songs to/from the playlist.\n"); printBlankSpaces(indent); printf(" - Press F4 for Track View.\n"); printBlankSpaces(indent); printf(" - Enter a number then Enter to switch song.\n"); printBlankSpaces(indent); printf(" - Space (or %s) to toggle pause.\n", settings->togglePause); printBlankSpaces(indent); printf(" - %s toggle color derived from album or from profile.\n", settings->toggleColorsDerivedFrom); printBlankSpaces(indent); printf(" - %s to update the library.\n", settings->updateLibrary); printBlankSpaces(indent); printf(" - %s to show/hide the spectrum visualizer.\n", settings->toggleVisualizer); printBlankSpaces(indent); printf(" - %s to toggle album covers drawn in ascii.\n", settings->toggleAscii); printBlankSpaces(indent); printf(" - %s to repeat the current song.\n", settings->toggleRepeat); printBlankSpaces(indent); printf(" - %s to shuffle the playlist.\n", settings->toggleShuffle); printBlankSpaces(indent); printf(" - %s to seek backward.\n", settings->seekBackward); printBlankSpaces(indent); printf(" - %s to seek forward.\n", settings->seekForward); printBlankSpaces(indent); printf(" - %s to save the playlist to your music folder.\n", settings->savePlaylist); printBlankSpaces(indent); printf(" - %s to add current song to kew.m3u (run with \"kew .\").\n", settings->addToMainPlaylist); printBlankSpaces(indent); printf(" - Esc or %s to quit.\n", settings->quit); printf("\n"); printLastRow(); numPrintedRows += 23; return numPrintedRows; } void toggleShowPlaylist() { refresh = true; if (appState.currentView == PLAYLIST_VIEW) { appState.currentView = SONG_VIEW; } else { appState.currentView = PLAYLIST_VIEW; } } void toggleShowSearch() { refresh = true; if (appState.currentView == SEARCH_VIEW) { appState.currentView = SONG_VIEW; } else { appState.currentView = SEARCH_VIEW; } } void toggleShowLibrary() { refresh = true; if (appState.currentView == LIBRARY_VIEW) { appState.currentView = SONG_VIEW; } else { appState.currentView = LIBRARY_VIEW; } } void tabNext() { if (appState.currentView == PLAYLIST_VIEW) appState.currentView = LIBRARY_VIEW; else if (appState.currentView == LIBRARY_VIEW) { if (currentSong != NULL) { appState.currentView = SONG_VIEW; } else { appState.currentView = SEARCH_VIEW; } } else if (appState.currentView == SONG_VIEW) appState.currentView = SEARCH_VIEW; else if (appState.currentView == SEARCH_VIEW) appState.currentView = KEYBINDINGS_VIEW; else if (appState.currentView == KEYBINDINGS_VIEW) appState.currentView = PLAYLIST_VIEW; refresh = true; } void showTrack() { refresh = true; appState.currentView = SONG_VIEW; } void toggleShowKeyBindings() { refresh = true; if (appState.currentView == KEYBINDINGS_VIEW) { appState.currentView = SONG_VIEW; } else { appState.currentView = KEYBINDINGS_VIEW; } } void flipNextPage() { if (appState.currentView == LIBRARY_VIEW) { chosenLibRow += maxLibListSize - 1; startLibIter += maxLibListSize - 1; refresh = true; } else if (appState.currentView == PLAYLIST_VIEW) { chosenRow += maxListSize - 1; chosenRow = (chosenRow >= originalPlaylist->count) ? originalPlaylist->count - 1 : chosenRow; refresh = true; } else if (appState.currentView == SEARCH_VIEW) { chosenSearchResultRow += maxSearchListSize -1; chosenSearchResultRow = (chosenSearchResultRow >= getSearchResultsCount()) ? getSearchResultsCount() - 1 : chosenSearchResultRow; startSearchIter += maxSearchListSize - 1; refresh = true; } } void flipPrevPage() { if (appState.currentView == LIBRARY_VIEW) { chosenLibRow -= maxLibListSize; startLibIter -= maxLibListSize; refresh = true; } else if (appState.currentView == PLAYLIST_VIEW) { chosenRow -= maxListSize; chosenRow = (chosenRow > 0) ? chosenRow : 0; refresh = true; } else if (appState.currentView == SEARCH_VIEW) { chosenSearchResultRow -= maxSearchListSize; chosenSearchResultRow = (chosenSearchResultRow > 0) ? chosenSearchResultRow : 0; startSearchIter -= maxSearchListSize; refresh = true; } } void scrollNext() { if (appState.currentView == PLAYLIST_VIEW) { chosenRow++; chosenRow = (chosenRow >= originalPlaylist->count) ? originalPlaylist->count - 1 : chosenRow; refresh = true; } else if (appState.currentView == LIBRARY_VIEW) { chosenLibRow++; refresh = true; } else if (appState.currentView == SEARCH_VIEW) { chosenSearchResultRow++; refresh = true; } } void scrollPrev() { if (appState.currentView == PLAYLIST_VIEW) { chosenRow--; chosenRow = (chosenRow > 0) ? chosenRow : 0; refresh = true; } else if (appState.currentView == LIBRARY_VIEW) { chosenLibRow--; refresh = true; } else if (appState.currentView == SEARCH_VIEW) { chosenSearchResultRow--; chosenSearchResultRow = ( chosenSearchResultRow > 0) ? chosenSearchResultRow : 0; refresh = true; } } int getRowWithinBounds(int row) { if (row >= originalPlaylist->count) { row = originalPlaylist->count - 1; } if (row < 0) row = 0; return row; } int printLogoAndAdjustments(SongData *songData, int termWidth, bool hideHelp, int indentation) { int aboutRows = printLogo(songData); if (termWidth > 52 && !hideHelp) { setDefaultTextColor(); printBlankSpaces(indentation); printf(" Use ↑, ↓ or k, j to choose. Enter to accept.\n"); printBlankSpaces(indentation); printf(" Pg Up and Pg Dn to scroll. Del to remove entry.\n\n"); return aboutRows + 3; } return aboutRows; } void showSearch(SongData *songData, int *chosenRow) { int term_w, term_h; getTermSize(&term_w, &term_h); maxSearchListSize = term_h - 5; int aboutRows = printLogo(songData); maxSearchListSize -= aboutRows; printBlankSpaces(indent); printf(" Use ↑, ↓ to choose. Enter to accept.\n\n"); maxSearchListSize -= 2; displaySearch(maxSearchListSize, indent, chosenRow, startSearchIter); printf("\n"); printLastRow(); } void showPlaylist(SongData *songData, PlayList *list, int *chosenSong, int *chosenNodeId) { int term_w, term_h; getTermSize(&term_w, &term_h); maxListSize = term_h - 2; int aboutRows = printLogoAndAdjustments(songData, term_w, hideHelp, indent); maxListSize -= aboutRows; displayPlaylist(list, maxListSize, indent, chosenSong, chosenNodeId, resetPlaylistDisplay); printf("\n"); printLastRow(); } void printElapsedBars(int elapsedBars) { printBlankSpaces(indent); printf(" "); for (int i = 0; i < numProgressBars; i++) { if (i == 0) { printf("■ "); } else if (i < elapsedBars) printf("■ "); else { printf("= "); } } printf("\n"); } void printVisualizer(double elapsedSeconds) { if (visualizerEnabled && appState.currentView == SONG_VIEW) { printf("\n"); int term_w, term_h; getTermSize(&term_w, &term_h); int visualizerWidth = (ABSOLUTE_MIN_WIDTH > preferredWidth) ? ABSOLUTE_MIN_WIDTH : preferredWidth; visualizerWidth = (visualizerWidth < textWidth && textWidth < term_w - 2) ? textWidth : visualizerWidth; visualizerWidth = (visualizerWidth > term_w - 2) ? term_w - 2 : visualizerWidth; numProgressBars = (int)visualizerWidth / 2; drawSpectrumVisualizer(visualizerHeight, visualizerWidth, color, indent, useProfileColors); printElapsedBars(calcElapsedBars(elapsedSeconds, duration, numProgressBars)); printLastRow(); int jumpAmount = visualizerHeight + 2; cursorJump(jumpAmount); saveCursorPosition(); } else if (!visualizerEnabled) { int term_w, term_h; getTermSize(&term_w, &term_h); if (term_w >= minWidth) { printf("\n\n"); printLastRow(); cursorJump(2); } } } void calcIndent(SongData *songdata) { if (songdata == NULL || appState.currentView != SONG_VIEW) { int textWidth = (ABSOLUTE_MIN_WIDTH > preferredWidth) ? ABSOLUTE_MIN_WIDTH : preferredWidth; indent = getIndentation(textWidth - 1) - 1; return; } int titleLength = strlen(songdata->metadata->title); int albumLength = strlen(songdata->metadata->album); int maxTextLength = (albumLength > titleLength) ? albumLength : titleLength; textWidth = (ABSOLUTE_MIN_WIDTH > preferredWidth) ? ABSOLUTE_MIN_WIDTH : preferredWidth; int term_w, term_h; getTermSize(&term_w, &term_h); int maxSize = term_w - 2; if (titleLength > 0 && titleLength < maxSize && titleLength > textWidth) textWidth = titleLength; if (maxTextLength > 0 && maxTextLength < maxSize && maxTextLength > textWidth) textWidth = maxTextLength; if (textWidth > maxSize) textWidth = maxSize; indent = getIndentation(textWidth - 1) - 1; } FileSystemEntry *getCurrentLibEntry() { return currentEntry; } FileSystemEntry *getLibrary() { return library; } FileSystemEntry *getChosenDir() { return chosenDir; } void processName(const char *name, char *output, int maxWidth) { char *lastDot = strrchr(name, '.'); if (lastDot != NULL) { int copyLength = lastDot - name; if (copyLength > maxWidth) { copyLength = maxWidth; } strncpy(output, name, copyLength); output[copyLength] = '\0'; removeUnneededChars(output); } else { strncpy(output, name, maxWidth); output[maxWidth] = '\0'; removeUnneededChars(output); } } void setChosenDir(FileSystemEntry *entry) { if (entry->isDirectory) { currentEntry = chosenDir = entry; } } void setCurrentAsChosenDir() { if (currentEntry->isDirectory) chosenDir = currentEntry; } void resetChosenDir() { chosenDir = NULL; } int displayTree(FileSystemEntry *root, int depth, int maxListSize, int maxNameWidth) { char dirName[maxNameWidth + 1]; char filename[maxNameWidth + 1]; bool foundChosen = false; int foundCurrent = 0; if (currentSong != NULL && (strcmp(currentSong->song.filePath, root->fullPath) == 0)) { foundCurrent = 1; } if (startLibIter < 0) startLibIter = 0; if (libIter >= startLibIter + maxListSize) { return false; } FileSystemEntry *tmp = root->parent == NULL ? NULL : root->parent->children; int numAudioChildren = 0; while (tmp != NULL) { if (!tmp->isDirectory) numAudioChildren++; tmp = tmp->next; } if (chosenLibRow > startLibIter + maxListSize - round(maxListSize / 2)) { startLibIter = chosenLibRow - maxListSize + round(maxListSize / 2) + 1; } if (allowChooseSongs) { if (chosenLibRow >= libIter + libSongIter && libSongIter != 0) { startLibIter = chosenLibRow - round(maxListSize / 2); } } else { if (chosenLibRow >= numDirectoryTreeEntries + numTopLevelSongs) { startLibIter = numDirectoryTreeEntries + numTopLevelSongs - maxListSize; chosenLibRow = numDirectoryTreeEntries + numTopLevelSongs - 1; } } if (chosenLibRow < 0) startLibIter = chosenLibRow = libIter = 0; if (root == NULL) return false; if (root->isDirectory || (!root->isDirectory && depth == 1) || (chosenDir != NULL && allowChooseSongs && root->parent != NULL && (strcmp(root->parent->fullPath, chosenDir->fullPath) == 0 || strcmp(root->fullPath, chosenDir->fullPath) == 0))) { if (depth > 0) { if (libIter >= startLibIter) { if (depth == 1) { if (useProfileColors) setTextColor(artistColor); else setColor(); } else { setDefaultTextColor(); } if (depth >= 2) printf(" "); printBlankSpaces(indent); if (chosenLibRow == libIter) { if (root->isEnqueued) { if (useProfileColors) setTextColor(enqueuedColor); else setColor(); printf("\x1b[7m * "); } else { printf(" \x1b[7m "); } currentEntry = root; if (allowChooseSongs == true && (chosenDir == NULL || (currentEntry != NULL && currentEntry->parent != NULL && chosenDir != NULL && (strcmp(currentEntry->parent->fullPath, chosenDir->fullPath) != 0) && strcmp(root->fullPath, chosenDir->fullPath) != 0))) { chosenLibRow -= libSongIter; allowChooseSongs = false; chosenDir = NULL; refresh = true; } foundChosen = true; } else { if (root->isEnqueued) { if (useProfileColors) printf("\033[%d;3%dm", foundCurrent, enqueuedColor); else setColorAndWeight(foundCurrent); printf(" * "); } else { printf(" "); } } if (root->isDirectory) { dirName[0] = '\0'; snprintf(dirName, maxNameWidth + 1, "%s", root->name); if (depth == 1) printf("%s \n", stringToUpper(dirName)); else printf("%s \n", dirName); } else { filename[0] = '\0'; processName(root->name, filename, maxNameWidth); printf(" └─%s \n", filename); libSongIter++; } setColor(); } libIter++; } FileSystemEntry *child = root->children; while (child != NULL) { if (displayTree(child, depth + 1, maxListSize, maxNameWidth)) foundChosen = true; child = child->next; } } return foundChosen; } char *getLibraryFilePath() { char *configdir = getConfigPath(); char *filepath = NULL; size_t filepath_length = strlen(configdir) + strlen("/") + strlen(LIBRARY_FILE) + 1; filepath = (char *)malloc(filepath_length); strcpy(filepath, configdir); strcat(filepath, "/"); strcat(filepath, LIBRARY_FILE); free(configdir); return filepath; } void showLibrary(SongData *songData) { libIter = 0; libSongIter = 0; startLibIter = 0; refresh = false; int term_w, term_h; getTermSize(&term_w, &term_h); int totalHeight = term_h; maxLibListSize = totalHeight; setColor(); int aboutSize = printLogo(songData); int maxNameWidth = term_w - 10 - indent; maxLibListSize -= aboutSize + 2; setDefaultTextColor(); if (term_w > 60 && !hideHelp) { maxLibListSize -= 3; printBlankSpaces(indent); printf(" Use ↑, ↓ or k, j to choose. Enter to enqueue/dequeue.\n"); printBlankSpaces(indent); printf(" Pg Up and Pg Dn to scroll. Press u to update the library.\n\n"); } numTopLevelSongs = 0; FileSystemEntry *tmp = library->children; while (tmp != NULL) { if (!tmp->isDirectory) numTopLevelSongs++; tmp = tmp->next; } bool foundChosen = displayTree(library, 0, maxLibListSize, maxNameWidth); if (!foundChosen) { chosenLibRow--; refresh = true; } printf("\n"); printLastRow(); if (refresh) { printf("\033[1;1H"); clearScreen(); showLibrary(songData); } } int printPlayer(SongData *songdata, double elapsedSeconds, AppSettings *settings) { if (!uiEnabled) { return 0; } hideCursor(); setColor(); if (songdata != NULL && songdata->metadata != NULL && !songdata->hasErrors && (songdata->hasErrors < 1)) { metadata = *songdata->metadata; duration = songdata->duration; if (songdata->cover != NULL && coverEnabled) { color.r = songdata->red; color.g = songdata->green; color.b = songdata->blue; } } else { if (appState.currentView != LIBRARY_VIEW && appState.currentView != PLAYLIST_VIEW && appState.currentView != SEARCH_VIEW && appState.currentView != KEYBINDINGS_VIEW) { appState.currentView = LIBRARY_VIEW; } color.r = defaultColor; color.g = defaultColor; color.b = defaultColor; } calcPreferredSize(); calcIndent(songdata); if (preferredWidth <= 0 || preferredHeight <= 0) return -1; if (appState.currentView != PLAYLIST_VIEW) resetPlaylistDisplay = true; if (appState.currentView == KEYBINDINGS_VIEW && refresh) { clearScreen(); showKeyBindings(songdata, settings); saveCursorPosition(); refresh = false; } else if (appState.currentView == PLAYLIST_VIEW && refresh) { clearScreen(); showPlaylist(songdata, originalPlaylist, &chosenRow, &chosenNodeId); resetPlaylistDisplay = false; refresh = false; } else if (appState.currentView == SEARCH_VIEW && (refresh || newUndisplayedSearch)) { clearScreen(); showSearch(songdata, &chosenSearchResultRow); refresh = false; newUndisplayedSearch = false; } else if (appState.currentView == LIBRARY_VIEW && refresh) { clearScreen(); showLibrary(songdata); refresh = false; } else if (appState.currentView == SONG_VIEW && songdata != NULL) { if (refresh) { clearScreen(); printf("\n"); printCover(songdata); printMetadata(songdata->metadata); refresh = false; } printTime(elapsedSeconds); printVisualizer(elapsedSeconds); } fflush(stdout); return 0; } void showHelp() { printHelp(); } void freeMainDirectoryTree() { if (library == NULL) return; char *filepath = getLibraryFilePath(); if (cacheLibrary) freeAndWriteTree(library, filepath); else freeTree(library); free(filepath); } kew-2.8.2/src/player.h000066400000000000000000000037561467402032100145570ustar00rootroot00000000000000#ifndef PLAYER_H #define PLAYER_H #include #include #include #include #include "../include/imgtotxt/write_ascii.h" #include "../include/imgtotxt/options.h" #include "chafafunc.h" #include "directorytree.h" #include "playlist.h" #include "playlist_ui.h" #include "search_ui.h" #include "songloader.h" #include "sound.h" #include "term.h" #include "utils.h" #include "visuals.h" #include "common_ui.h" extern const char VERSION[]; extern bool coverEnabled; extern bool uiEnabled; extern bool coverAnsi; extern bool visualizerEnabled; extern bool useThemeColors; extern bool hasPrintedPaused; extern bool quitAfterStopping; extern bool nerdFontsEnabled; extern bool hideLogo; extern bool hideHelp; extern int numProgressBars; extern int chosenSong; extern bool resetPlaylistDisplay; extern int visualizerHeight; extern TagSettings metadata; extern bool fastForwarding; extern bool rewinding; extern double elapsedSeconds; extern double pauseSeconds; extern double totalPauseSeconds; extern double seekAccumulatedSeconds; extern bool allowChooseSongs; extern int chosenLibRow; extern int chosenSearchResultRow; extern int chosenRow; extern int chosenNodeId; extern int cacheLibrary; extern int numDirectoryTreeEntries; extern FileSystemEntry *library; bool hasNerdFonts(); int printPlayer(SongData *songdata, double elapsedSeconds, AppSettings *settings); void flipNextPage(); void flipPrevPage(); void showHelp(void); void setChosenDir(FileSystemEntry *entry); int printAbout(SongData *songdata); FileSystemEntry *getCurrentLibEntry(); FileSystemEntry *getChosenDir(); FileSystemEntry *getLibrary(); void scrollNext(void); void scrollPrev(void); void setCurrentAsChosenDir(); void toggleShowKeyBindings(void); void toggleShowLibrary(); void toggleShowPlaylist(void); void toggleShowSearch(void); void showTrack(); void setTextColorRGB2(int r, int g, int b); void freeMainDirectoryTree(); char *getLibraryFilePath(); void resetChosenDir(); void tabNext(); #endif kew-2.8.2/src/playerops.c000066400000000000000000001344201467402032100152650ustar00rootroot00000000000000#include "playerops.h" /* playerops.c Related to features/actions of the player. */ #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif #ifndef ASK_IF_USE_CACHE_LIMIT_SECONDS #define ASK_IF_USE_CACHE_LIMIT_SECONDS 4 #endif struct timespec current_time; struct timespec start_time; struct timespec pause_time; struct timespec lastInputTime; struct timespec lastPlaylistChangeTime; struct timespec lastUpdateTime = {0, 0}; bool playlistNeedsUpdate = false; bool nextSongNeedsRebuilding = false; bool enqueuedNeedsUpdate = false; bool skipFromStopped = false; bool playingMainPlaylist = false; bool usingSongDataA = true; bool doNotifyMPRISSwitched = false; LoadingThreadData loadingdata; volatile bool loadedNextSong = false; bool waitingForPlaylist = false; bool waitingForNext = false; Node *nextSong = NULL; Node *tryNextSong = NULL; Node *songToStartFrom = NULL; Node *prevSong = NULL; int lastPlayedId = -1; bool songHasErrors = false; bool skipOutOfOrder = false; bool skipping = false; bool loadingFailed = false; bool forceSkip = false; volatile bool clearingErrors = false; volatile bool songLoading = false; GDBusConnection *connection = NULL; void reshufflePlaylist() { playlistNeedsUpdate = false; if (isShuffleEnabled()) { if (currentSong != NULL) shufflePlaylistStartingFromSong(&playlist, currentSong); else shufflePlaylist(&playlist); nextSongNeedsRebuilding = true; } } void rebuildAndUpdatePlaylist() { if (!playlistNeedsUpdate && !nextSongNeedsRebuilding) return; pthread_mutex_lock(&(playlist.mutex)); if (playlistNeedsUpdate) { reshufflePlaylist(); } pthread_mutex_unlock(&(playlist.mutex)); } void skip() { setCurrentImplementationType(NONE); setRepeatEnabled(false); audioData.endOfListReached = false; rebuildAndUpdatePlaylist(); if (!isPlaying()) { switchAudioImplementation(); skipFromStopped = true; } else { setSkipToNext(true); } if (!skipOutOfOrder) refresh = true; } void updateLastSongSwitchTime() { clock_gettime(CLOCK_MONOTONIC, &start_time); } void updateLastPlaylistChangeTime() { clock_gettime(CLOCK_MONOTONIC, &lastPlaylistChangeTime); } void updateLastInputTime() { clock_gettime(CLOCK_MONOTONIC, &lastInputTime); } void updatePlaybackPosition(double elapsedSeconds) { GVariantBuilder changedPropertiesBuilder; g_variant_builder_init(&changedPropertiesBuilder, G_VARIANT_TYPE_DICTIONARY); g_variant_builder_add(&changedPropertiesBuilder, "{sv}", "Position", g_variant_new_int64(llround(elapsedSeconds * G_USEC_PER_SEC))); GVariant *parameters = g_variant_new("(sa{sv}as)", "org.mpris.MediaPlayer2.Player", &changedPropertiesBuilder, NULL); g_dbus_connection_emit_signal(connection, NULL, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "PropertiesChanged", parameters, NULL); } void emitSeekedSignal(double newPositionSeconds) { gint64 newPositionMicroseconds = llround(newPositionSeconds * G_USEC_PER_SEC); GVariant *parameters = g_variant_new("(x)", newPositionMicroseconds); g_dbus_connection_emit_signal(connection, NULL, "/org/mpris/MediaPlayer2", "org.mpris.MediaPlayer2.Player", "Seeked", parameters, NULL); } void emitStringPropertyChanged(const gchar *propertyName, const gchar *newValue) { GVariantBuilder changed_properties_builder; g_variant_builder_init(&changed_properties_builder, G_VARIANT_TYPE("a{sv}")); g_variant_builder_add(&changed_properties_builder, "{sv}", propertyName, g_variant_new_string(newValue)); g_dbus_connection_emit_signal(connection, NULL, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "PropertiesChanged", g_variant_new("(sa{sv}as)", "org.mpris.MediaPlayer2.Player", &changed_properties_builder, NULL), NULL); g_variant_builder_clear(&changed_properties_builder); } void emitBooleanPropertyChanged(const gchar *propertyName, gboolean newValue) { GVariantBuilder changed_properties_builder; g_variant_builder_init(&changed_properties_builder, G_VARIANT_TYPE("a{sv}")); g_variant_builder_add(&changed_properties_builder, "{sv}", propertyName, g_variant_new_boolean(newValue)); g_dbus_connection_emit_signal(connection, NULL, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "PropertiesChanged", g_variant_new("(sa{sv}as)", "org.mpris.MediaPlayer2.Player", &changed_properties_builder, NULL), NULL); g_variant_builder_clear(&changed_properties_builder); } void playbackPause(struct timespec *pause_time) { if (!isPaused()) { emitStringPropertyChanged("PlaybackStatus", "Paused"); clock_gettime(CLOCK_MONOTONIC, pause_time); } pausePlayback(); } void skipToSong(int id, bool startPlaying) { if (songLoading || !loadedNextSong || skipping || clearingErrors) if (!forceSkip) return; Node *found = NULL; findNodeInList(&playlist, id, &found); if (found != NULL) currentSong = found; else { return; } skipping = true; skipOutOfOrder = true; loadedNextSong = false; songLoading = true; forceSkip = false; if (startPlaying) { playbackPlay(&totalPauseSeconds, &pauseSeconds); } // cancel starting from top if (waitingForPlaylist || audioData.restart) { waitingForPlaylist = false; audioData.restart = false; if (isShuffleEnabled()) reshufflePlaylist(); } loadingdata.loadA = !usingSongDataA; loadingdata.loadingFirstDecoder = true; loadSong(currentSong, &loadingdata); int maxNumTries = 50; int numtries = 0; while (!loadedNextSong && !loadingFailed && numtries < maxNumTries) { c_sleep(100); numtries++; } if (songHasErrors) { songHasErrors = false; forceSkip = true; if (currentSong->next != NULL) skipToSong(currentSong->next->id, true); } updateLastSongSwitchTime(); skip(); } void skipToBegginningOfSong() { if (currentSong != NULL) { bool playSong = false; skipToSong(currentSong->id, playSong); } } void prepareIfSkippedSilent() { if (hasSilentlySwitched) { skipping = true; hasSilentlySwitched = false; updateLastSongSwitchTime(); setCurrentImplementationType(NONE); setRepeatEnabled(false); audioData.endOfListReached = false; rebuildAndUpdatePlaylist(); switchAudioImplementation(); usingSongDataA = !usingSongDataA; skipping = false; } } void playbackPlay(double *totalPauseSeconds, double *pauseSeconds) { if (isPaused()) { *totalPauseSeconds += *pauseSeconds; emitStringPropertyChanged("PlaybackStatus", "Playing"); } else if (isStopped()) { emitStringPropertyChanged("PlaybackStatus", "Playing"); } if (isStopped() && !hasSilentlySwitched) { skipToBegginningOfSong(); } resumePlayback(); if (hasSilentlySwitched) { *totalPauseSeconds = 0; prepareIfSkippedSilent(); } } void togglePause(double *totalPauseSeconds, double *pauseSeconds, struct timespec *pause_time) { togglePausePlayback(); if (isPaused()) { emitStringPropertyChanged("PlaybackStatus", "Paused"); clock_gettime(CLOCK_MONOTONIC, pause_time); } else { if (hasSilentlySwitched && !skipping) { *totalPauseSeconds = 0; prepareIfSkippedSilent(); } else { *totalPauseSeconds += *pauseSeconds; } emitStringPropertyChanged("PlaybackStatus", "Playing"); } } void toggleRepeat() { bool repeatEnabled = !isRepeatEnabled(); setRepeatEnabled(repeatEnabled); if (repeatEnabled) { emitStringPropertyChanged("LoopStatus", "Track"); } else { emitStringPropertyChanged("LoopStatus", "None"); } if (appState.currentView != SONG_VIEW) refresh = true; } void addToSpecialPlaylist() { if (currentSong == NULL) return; int id = currentSong->id; Node *node = NULL; if (findSelectedEntryById(specialPlaylist, id) != NULL) // song is already in list return; createNode(&node, currentSong->song.filePath, id); addToList(specialPlaylist, node); } void toggleShuffle() { bool shuffleEnabled = !isShuffleEnabled(); setShuffleEnabled(shuffleEnabled); if (shuffleEnabled) { pthread_mutex_lock(&(playlist.mutex)); shufflePlaylistStartingFromSong(&playlist, currentSong); pthread_mutex_unlock(&(playlist.mutex)); emitBooleanPropertyChanged("Shuffle", TRUE); } else { char *path = NULL; if (currentSong != NULL) { path = strdup(currentSong->song.filePath); } pthread_mutex_lock(&(playlist.mutex)); deletePlaylist(&playlist); // Doesn't destroy the mutex deepCopyPlayListOntoList(originalPlaylist, &playlist); if (path != NULL) { currentSong = findPathInPlaylist(path, &playlist); free(path); } pthread_mutex_unlock(&(playlist.mutex)); emitBooleanPropertyChanged("Shuffle", FALSE); } loadedNextSong = false; nextSong = NULL; if (appState.currentView == PLAYLIST_VIEW || appState.currentView == LIBRARY_VIEW) refresh = true; } void toggleBlocks(AppSettings *settings) { coverAnsi = !coverAnsi; c_strcpy(settings->coverAnsi, sizeof(settings->coverAnsi), coverAnsi ? "1" : "0"); if (coverEnabled) { clearScreen(); refresh = true; } } void toggleColors(AppSettings *settings) { useProfileColors = !useProfileColors; c_strcpy(settings->useProfileColors, sizeof(settings->useProfileColors), useProfileColors ? "1" : "0"); clearScreen(); refresh = true; } void toggleVisualizer(AppSettings *settings) { visualizerEnabled = !visualizerEnabled; c_strcpy(settings->visualizerEnabled, sizeof(settings->visualizerEnabled), visualizerEnabled ? "1" : "0"); restoreCursorPosition(); refresh = true; } void quit() { doQuit = true; } bool isCurrentSongDeleted() { return (audioData.currentFileIndex == 0) ? userData.songdataADeleted == true : userData.songdataBDeleted == true; } bool isValidSong(SongData *songData) { return songData != NULL && songData->hasErrors == false && songData->metadata != NULL; } SongData *getCurrentSongData(void) { if (currentSong == NULL) return NULL; if (isCurrentSongDeleted()) return NULL; SongData *songData = NULL; bool isDeleted = determineCurrentSongData(&songData); if (isDeleted) return NULL; if (!isValidSong(songData)) return NULL; return songData; } void calcElapsedTime() { if (isStopped()) return; clock_gettime(CLOCK_MONOTONIC, ¤t_time); double timeSinceLastUpdate = (double)(current_time.tv_sec - lastUpdateTime.tv_sec) + (double)(current_time.tv_nsec - lastUpdateTime.tv_nsec) / 1e9; if (!isPaused()) { elapsedSeconds = (double)(current_time.tv_sec - start_time.tv_sec) + (double)(current_time.tv_nsec - start_time.tv_nsec) / 1e9; double seekElapsed = getSeekElapsed(); double diff = elapsedSeconds + (seekElapsed + seekAccumulatedSeconds - totalPauseSeconds); if (diff < 0) seekElapsed -= diff; elapsedSeconds += seekElapsed + seekAccumulatedSeconds - totalPauseSeconds; if (elapsedSeconds > duration) elapsedSeconds = duration; setSeekElapsed(seekElapsed); if (elapsedSeconds < 0.0) { elapsedSeconds = 0.0; } if (currentSong != NULL && timeSinceLastUpdate >= 1.0) { lastUpdateTime = current_time; } } else { pauseSeconds = (double)(current_time.tv_sec - pause_time.tv_sec) + (double)(current_time.tv_nsec - pause_time.tv_nsec) / 1e9; } } void flushSeek() { if (seekAccumulatedSeconds != 0.0) { if (currentSong != NULL) { if (endsWith(currentSong->song.filePath, "ogg")) { return; } } setSeekElapsed(getSeekElapsed() + seekAccumulatedSeconds); seekAccumulatedSeconds = 0.0; calcElapsedTime(); float percentage = elapsedSeconds / (float)duration * 100.0; if (percentage < 0.0) { setSeekElapsed(0.0); percentage = 0.0; } seekPercentage(percentage); emitSeekedSignal(elapsedSeconds); } } bool setPosition(gint64 newPosition) { gint64 currentPositionMicroseconds = llround(elapsedSeconds * G_USEC_PER_SEC); if (duration != 0.0) { gint64 step = newPosition - currentPositionMicroseconds; step = step / G_USEC_PER_SEC; seekAccumulatedSeconds += step; return true; } else { return false; } } bool seekPosition(gint64 offset) { if (duration != 0.0) { gint64 step = offset; step = step / G_USEC_PER_SEC; seekAccumulatedSeconds += step; return true; } else { return false; } } void seekForward() { if (currentSong != NULL) { if (endsWith(currentSong->song.filePath, "ogg")) { return; } } if (duration != 0.0) { float step = 100 / numProgressBars; seekAccumulatedSeconds += step * duration / 100.0; } fastForwarding = true; } void seekBack() { if (currentSong != NULL) { if (endsWith(currentSong->song.filePath, "ogg")) { return; } } if (duration != 0.0) { float step = 100 / numProgressBars; seekAccumulatedSeconds -= step * duration / 100.0; } rewinding = true; } Node *findSelectedEntryById(PlayList *playlist, int id) { Node *node = playlist->head; if (node == NULL || id < 0) return NULL; bool found = false; for (int i = 0; i < playlist->count; i++) { if (node != NULL && node->id == id) { found = true; break; } else if (node == NULL) { return NULL; } node = node->next; } if (found) { return node; } return NULL; } Node *findSelectedEntry(PlayList *playlist, int row) { Node *node = playlist->head; if (node == NULL) return NULL; bool found = false; for (int i = 0; i < playlist->count; i++) { if (i == row) { found = true; break; } node = node->next; } if (found) { return node; } return NULL; } bool markAsDequeued(FileSystemEntry *root, char *path) { int numChildrenEnqueued = 0; if (root == NULL) return false; if (!root->isDirectory) { if (strcmp(root->fullPath, path) == 0) { root->isEnqueued = false; return true; } } else { FileSystemEntry *child = root->children; bool found = false; while (child != NULL) { found = markAsDequeued(child, path); child = child->next; if (found) break; } if (found) { child = root->children; while (child != NULL) { if (child->isEnqueued) numChildrenEnqueued++; child = child->next; } if (numChildrenEnqueued == 0) root->isEnqueued = false; return true; } } return false; } Node *getNextSong() { if (nextSong != NULL) return nextSong; else if (currentSong != NULL && currentSong->next != NULL) { return currentSong->next; } else { return NULL; } } void enqueueSong(FileSystemEntry *child) { int id = nodeIdCounter++; Node *node = NULL; createNode(&node, child->fullPath, id); addToList(originalPlaylist, node); Node *node2 = NULL; createNode(&node2, child->fullPath, id); addToList(&playlist, node2); child->isEnqueued = 1; child->parent->isEnqueued = 1; } void silentSwitchToNext(bool loadSong) { skipping = true; nextSong = NULL; setCurrentSongToNext(); activateSwitch(&audioData); skipOutOfOrder = true; usingSongDataA = (audioData.currentFileIndex == 0); if (loadSong) { loadNextSong(); finishLoading(); loadedNextSong = true; doNotifyMPRISSwitched = true; } resetTimeCount(); clock_gettime(CLOCK_MONOTONIC, &start_time); refresh = true; skipping = false; hasSilentlySwitched = true; } void removeCurrentlyPlayingSong() { stopPlayback(); emitStringPropertyChanged("PlaybackStatus", "Stopped"); clearCurrentTrack(); loadedNextSong = false; audioData.restart = true; audioData.endOfListReached = true; lastPlayedId = currentSong->id; songToStartFrom = getListNext(currentSong); waitingForNext = true; currentSong = NULL; } void dequeueSong(FileSystemEntry *child) { Node *node1 = findLastPathInPlaylist(child->fullPath, originalPlaylist); if (node1 == NULL) return; if (currentSong != NULL && currentSong->id == node1->id) { removeCurrentlyPlayingSong(); } else { if (songToStartFrom != NULL) { songToStartFrom = getListNext(node1); } } int id = node1->id; Node *node2 = findSelectedEntryById(&playlist, id); if (node1 != NULL) deleteFromList(originalPlaylist, node1); if (node2 != NULL) deleteFromList(&playlist, node2); child->isEnqueued = 0; // check if parent needs to be dequeued as well bool isEnqueued = false; FileSystemEntry *ch = child->parent->children; while (ch != NULL) { if (ch->isEnqueued) { isEnqueued = true; break; } ch = ch->next; } if (!isEnqueued) { child->parent->isEnqueued = 0; if (child->parent->parent != NULL) child->parent->parent->isEnqueued = 0; } } void dequeueChildren(FileSystemEntry *parent) { FileSystemEntry *child = parent->children; while (child != NULL) { if (child->isDirectory && child->children != NULL) { dequeueChildren(child); } else { dequeueSong(child); } child = child->next; } } void enqueueChildren(FileSystemEntry *child) { while (child != NULL) { if (child->isDirectory && child->children != NULL) { child->isEnqueued = 1; enqueueChildren(child->children); } else if (!child->isEnqueued) { enqueueSong(child); } child = child->next; } } bool hasSongChildren(FileSystemEntry *entry) { FileSystemEntry *child = entry->children; int numSongs = 0; while (child != NULL) { if (!child->isDirectory) numSongs++; child = child->next; } if (numSongs == 0) { return false; } return true; } bool hasDequeuedChildren(FileSystemEntry *parent) { FileSystemEntry *child = parent->children; bool isDequeued = false; while (child != NULL) { if (!child->isEnqueued) { isDequeued = true; } child = child->next; } return isDequeued; } void enqueueSongs(FileSystemEntry *entry) { FileSystemEntry *chosenDir = getChosenDir(); bool hasEnqueued = false; if (entry != NULL) { if (entry->isDirectory) { if (!hasSongChildren(entry) || (chosenDir != NULL && strcmp(entry->fullPath, chosenDir->fullPath) == 0)) { if (hasDequeuedChildren(entry)) { entry->isEnqueued = 1; entry = entry->children; enqueueChildren(entry); nextSongNeedsRebuilding = true; hasEnqueued = true; } else { dequeueChildren(entry); nextSongNeedsRebuilding = true; } } setCurrentAsChosenDir(); allowChooseSongs = true; } else { if (!entry->isEnqueued) { nextSong = NULL; nextSongNeedsRebuilding = true; enqueueSong(entry); hasEnqueued = true; } else { nextSong = NULL; nextSongNeedsRebuilding = true; dequeueSong(entry); } } refresh = true; } if (hasEnqueued) { waitingForNext = true; audioData.endOfListReached = false; } if (nextSongNeedsRebuilding) { reshufflePlaylist(); } updateLastPlaylistChangeTime(); } void handleRemove() { if (refresh) return; if (appState.currentView != PLAYLIST_VIEW) { return; } bool rebuild = false; Node *node = findSelectedEntry(originalPlaylist, chosenRow); if (node == NULL) { return; } Node *song = getNextSong(); int id = node->id; int currentId = (currentSong != NULL) ? currentSong->id : -1; if (currentId == node->id) { removeCurrentlyPlayingSong(); } else { if (songToStartFrom != NULL) { songToStartFrom = getListNext(node); } } pthread_mutex_lock(&(playlist.mutex)); if (node != NULL && song != NULL && currentSong != NULL) { if (strcmp(song->song.filePath, node->song.filePath) == 0 || (currentSong != NULL && currentSong->next != NULL && id == currentSong->next->id)) rebuild = true; } if (node != NULL) markAsDequeued(getLibrary(), node->song.filePath); Node *node2 = findSelectedEntryById(&playlist, id); if (node != NULL) deleteFromList(originalPlaylist, node); if (node2 != NULL) deleteFromList(&playlist, node2); updateLastPlaylistChangeTime(); if (isShuffleEnabled()) rebuild = true; currentSong = findSelectedEntryById(&playlist, currentId); if (rebuild && currentSong != NULL) { node = NULL; nextSong = NULL; reshufflePlaylist(); tryNextSong = currentSong->next; nextSongNeedsRebuilding = false; nextSong = NULL; nextSong = getListNext(currentSong); rebuildNextSong(nextSong); loadedNextSong = true; } pthread_mutex_unlock(&(playlist.mutex)); refresh = true; } Node *getSongByNumber(PlayList *playlist, int songNumber) { Node *song = playlist->head; if (!song) return currentSong; if (songNumber <= 0) { return song; } int count = 1; while (song->next != NULL && count != songNumber) { song = getListNext(song); count++; } return song; } int loadDecoder(SongData *songData, bool *songDataDeleted) { int result = 0; if (songData != NULL) { *songDataDeleted = false; // this should only be done for the second song, as switchAudioImplementation() handles the first one if (!loadingdata.loadingFirstDecoder) { if (hasBuiltinDecoder(songData->filePath)) result = prepareNextDecoder(songData->filePath); else if (endsWith(songData->filePath, "opus")) result = prepareNextOpusDecoder(songData->filePath); else if (endsWith(songData->filePath, "ogg")) result = prepareNextVorbisDecoder(songData->filePath); else if (endsWith(songData->filePath, "m4a") || endsWith(songData->filePath, "aac")) result = prepareNextM4aDecoder(songData->filePath); } } return result; } int assignLoadedData() { int result = 0; if (loadingdata.loadA) { userData.songdataA = loadingdata.songdataA; result = loadDecoder(loadingdata.songdataA, &userData.songdataADeleted); } else { userData.songdataB = loadingdata.songdataB; result = loadDecoder(loadingdata.songdataB, &userData.songdataBDeleted); } return result; } void *songDataReaderThread(void *arg) { LoadingThreadData *loadingdata = (LoadingThreadData *)arg; // Acquire the mutex lock pthread_mutex_lock(&(loadingdata->mutex)); char filepath[MAXPATHLEN]; c_strcpy(filepath, sizeof(filepath), loadingdata->filePath); SongData *songdata = NULL; if (loadingdata->loadA) { if (!userData.songdataADeleted) { userData.songdataADeleted = true; unloadSongData(&loadingdata->songdataA); } } else { if (!userData.songdataBDeleted) { userData.songdataBDeleted = true; unloadSongData(&loadingdata->songdataB); } } if (filepath[0] != '\0') { songdata = loadSongData(filepath); } else songdata = NULL; if (loadingdata->loadA) { loadingdata->songdataA = songdata; } else { loadingdata->songdataB = songdata; } int result = assignLoadedData(); if (result < 0) songdata->hasErrors = true; // Release the mutex lock pthread_mutex_unlock(&(loadingdata->mutex)); if (songdata != NULL && songdata->hasErrors) { songHasErrors = true; clearingErrors = true; nextSong = NULL; } else { songHasErrors = false; clearingErrors = false; nextSong = tryNextSong; tryNextSong = NULL; } loadedNextSong = true; skipping = false; songLoading = false; return NULL; } void loadSong(Node *song, LoadingThreadData *loadingdata) { if (song == NULL) { loadedNextSong = true; skipping = false; songLoading = false; return; } c_strcpy(loadingdata->filePath, sizeof(loadingdata->filePath), song->song.filePath); pthread_t loadingThread; pthread_create(&loadingThread, NULL, songDataReaderThread, (void *)loadingdata); } void loadNext(LoadingThreadData *loadingdata) { nextSong = getListNext(currentSong); if (nextSong == NULL) { c_strcpy(loadingdata->filePath, sizeof(loadingdata->filePath), ""); } else { c_strcpy(loadingdata->filePath, sizeof(loadingdata->filePath), nextSong->song.filePath); } pthread_t loadingThread; pthread_create(&loadingThread, NULL, songDataReaderThread, (void *)loadingdata); } void rebuildNextSong(Node *song) { if (song == NULL) return; loadingdata.loadA = !usingSongDataA; loadingdata.loadingFirstDecoder = false; songLoading = true; loadSong(song, &loadingdata); int maxNumTries = 50; int numtries = 0; while (songLoading && !loadedNextSong && !loadingFailed && numtries < maxNumTries) { c_sleep(100); numtries++; } songLoading = false; } void stop() { stopPlayback(); if (isStopped()) { emitStringPropertyChanged("PlaybackStatus", "Stopped"); } } void loadNextSong() { songLoading = true; nextSongNeedsRebuilding = false; skipFromStopped = false; loadingdata.loadA = !usingSongDataA; tryNextSong = nextSong = getListNext(currentSong); loadingdata.loadingFirstDecoder = false; loadSong(nextSong, &loadingdata); } bool determineCurrentSongData(SongData **currentSongData) { *currentSongData = (audioData.currentFileIndex == 0) ? userData.songdataA : userData.songdataB; bool isDeleted = (audioData.currentFileIndex == 0) ? userData.songdataADeleted == true : userData.songdataBDeleted == true; if (isDeleted) { *currentSongData = (audioData.currentFileIndex != 0) ? userData.songdataA : userData.songdataB; isDeleted = (audioData.currentFileIndex != 0) ? userData.songdataADeleted == true : userData.songdataBDeleted == true; if (!isDeleted) { activateSwitch(&audioData); audioData.switchFiles = false; } } return isDeleted; } void setCurrentSongToNext() { if (currentSong != NULL) lastPlayedId = currentSong->id; currentSong = getNextSong(); } void finishLoading() { int maxNumTries = 20; int numtries = 0; while (!loadedNextSong && !loadingFailed && numtries < maxNumTries) { c_sleep(100); numtries++; } loadedNextSong = true; } void resetTimeCount() { elapsedSeconds = 0.0; pauseSeconds = 0.0; totalPauseSeconds = 0.0; } void skipToNextSong() { // Stop if there is no song or no next song if (currentSong == NULL || currentSong->next == NULL) { if (!isStopped() && !isPaused()) stop(); return; } if (songLoading || nextSongNeedsRebuilding || skipping || clearingErrors) return; if (isStopped() || isPaused()) { // FIXME: Emit MPRIS signal silentSwitchToNext(true); return; } playbackPlay(&totalPauseSeconds, &pauseSeconds); skipping = true; skipOutOfOrder = false; updateLastSongSwitchTime(); skip(); } void setCurrentSongToPrev() { if (isShuffleEnabled() && currentSong != NULL && currentSong->prev == NULL) { if (currentSong->prev == NULL && currentSong->next != NULL) currentSong = currentSong->next; else return; } else currentSong = currentSong->prev; } void silentSwitchToPrev() { skipping = true; setCurrentSongToPrev(); activateSwitch(&audioData); loadedNextSong = false; songLoading = true; forceSkip = false; usingSongDataA = !usingSongDataA; loadingdata.loadA = usingSongDataA; loadingdata.loadingFirstDecoder = true; loadSong(currentSong, &loadingdata); doNotifyMPRISSwitched = true; finishLoading(); resetTimeCount(); clock_gettime(CLOCK_MONOTONIC, &start_time); refresh = true; skipping = false; skipOutOfOrder = true; hasSilentlySwitched = true; } void skipToPrevSong() { // Stop if there is no song or no previous song if ((currentSong == NULL || currentSong->prev == NULL) && !isShuffleEnabled()) { if (!isStopped() && !isPaused()) stop(); return; } if (songLoading || skipping || clearingErrors) if (!forceSkip) return; if (isStopped() || isPaused()) { silentSwitchToPrev(); return; } setCurrentSongToPrev(); playbackPlay(&totalPauseSeconds, &pauseSeconds); skipping = true; skipOutOfOrder = true; loadedNextSong = false; songLoading = true; forceSkip = false; loadingdata.loadA = !usingSongDataA; loadingdata.loadingFirstDecoder = true; loadSong(currentSong, &loadingdata); int maxNumTries = 50; int numtries = 0; while (!loadedNextSong && !loadingFailed && numtries < maxNumTries) { c_sleep(100); numtries++; } if (songHasErrors) { songHasErrors = false; forceSkip = true; skipToPrevSong(); } updateLastSongSwitchTime(); skip(); } void skipToNumberedSong(int songNumber) { if (songLoading || !loadedNextSong || skipping || clearingErrors) if (!forceSkip) return; playbackPlay(&totalPauseSeconds, &pauseSeconds); skipping = true; skipOutOfOrder = true; loadedNextSong = false; songLoading = true; forceSkip = false; currentSong = getSongByNumber(originalPlaylist, songNumber); loadingdata.loadA = !usingSongDataA; loadingdata.loadingFirstDecoder = true; loadSong(currentSong, &loadingdata); int maxNumTries = 50; int numtries = 0; while (!loadedNextSong && !loadingFailed && numtries < maxNumTries) { c_sleep(100); numtries++; } if (songHasErrors) { songHasErrors = false; forceSkip = true; if (songNumber < playlist.count) skipToNumberedSong(songNumber + 1); } updateLastSongSwitchTime(); skip(); } void skipToLastSong() { Node *song = playlist.head; if (!song) return; int count = 1; while (song->next != NULL) { song = getListNext(song); count++; } skipToNumberedSong(count); } void loadFirstSong(Node *song) { if (song == NULL) return; loadingdata.loadingFirstDecoder = true; loadSong(song, &loadingdata); int i = 0; while (!loadedNextSong) { if (i != 0 && i % 1000 == 0 && uiEnabled) printf("."); i++; c_sleep(10); fflush(stdout); } } void unloadSongA() { if (userData.songdataADeleted == false) { userData.songdataADeleted = true; unloadSongData(&loadingdata.songdataA); userData.songdataA = NULL; } } void unloadSongB() { if (userData.songdataBDeleted == false) { userData.songdataBDeleted = true; unloadSongData(&loadingdata.songdataB); userData.songdataB = NULL; } } void unloadPreviousSong() { pthread_mutex_lock(&(loadingdata.mutex)); if (usingSongDataA && (skipping || (userData.currentSongData == NULL || userData.songdataADeleted == false || (loadingdata.songdataA != NULL && userData.songdataADeleted == false && userData.currentSongData->hasErrors == 0 && userData.currentSongData->trackId != NULL && strcmp(loadingdata.songdataA->trackId, userData.currentSongData->trackId) != 0)))) { unloadSongA(); if (!audioData.endOfListReached) loadedNextSong = false; usingSongDataA = false; } else if (!usingSongDataA && (skipping || (userData.currentSongData == NULL || userData.songdataBDeleted == false || (loadingdata.songdataB != NULL && userData.songdataBDeleted == false && userData.currentSongData->hasErrors == 0 && userData.currentSongData->trackId != NULL && strcmp(loadingdata.songdataB->trackId, userData.currentSongData->trackId) != 0)))) { unloadSongB(); if (!audioData.endOfListReached) loadedNextSong = false; usingSongDataA = true; } pthread_mutex_unlock(&(loadingdata.mutex)); } int loadFirst(Node *song) { loadFirstSong(song); usingSongDataA = true; while (songHasErrors && currentSong->next != NULL) { songHasErrors = false; loadedNextSong = false; currentSong = currentSong->next; loadFirstSong(currentSong); } if (songHasErrors) { // couldn't play any of the songs unloadPreviousSong(); currentSong = NULL; songHasErrors = false; return -1; } userData.currentPCMFrame = 0; userData.currentSongData = userData.songdataA; return 0; } void *updateLibraryThread(void *arg) { char *path = (char *)arg; int tmpDirectoryTreeEntries = 0; FileSystemEntry *temp = createDirectoryTree(path, &tmpDirectoryTreeEntries); pthread_mutex_lock(&switchMutex); copyIsEnqueued(library, temp); freeTree(library); library = temp; numDirectoryTreeEntries = tmpDirectoryTreeEntries; resetChosenDir(); pthread_mutex_unlock(&switchMutex); refresh = true; return NULL; } void updateLibrary(char *path) { pthread_t threadId; freeSearchResults(); if (pthread_create(&threadId, NULL, updateLibraryThread, path) != 0) { perror("Failed to create thread"); return; } } void askIfCacheLibrary() { if (cacheLibrary > -1) // Only use this function if cacheLibrary isn't set return; char input = '\0'; restoreTerminalMode(); enableInputBuffering(); showCursor(); printf("Would you like to enable a (local) library cache for quicker startup times?\nYou can update the cache at any time by pressing 'u'. (y/n): "); fflush(stdout); do { int res = scanf(" %c", &input); if (res < 0) break; } while (input != 'Y' && input != 'y' && input != 'N' && input != 'n'); if (input == 'Y' || input == 'y') { printf("Y\n"); cacheLibrary = 1; } else { printf("N\n"); cacheLibrary = 0; } setNonblockingMode(); disableInputBuffering(); hideCursor(); } void createLibrary(AppSettings *settings) { if (cacheLibrary > 0) { char *libFilepath = getLibraryFilePath(); library = reconstructTreeFromFile(libFilepath, settings->path, &numDirectoryTreeEntries); free(libFilepath); updateLibraryIfChangedDetected(); } if (library == NULL || library->children == NULL) { struct timeval start, end; gettimeofday(&start, NULL); library = createDirectoryTree(settings->path, &numDirectoryTreeEntries); gettimeofday(&end, NULL); long seconds = end.tv_sec - start.tv_sec; long microseconds = end.tv_usec - start.tv_usec; double elapsed = seconds + microseconds * 1e-6; // If time to load the library was significant, ask to use cache instead if (elapsed > ASK_IF_USE_CACHE_LIMIT_SECONDS) { askIfCacheLibrary(); } } if (library == NULL || library->children == NULL) { exit(0); } } time_t getModificationTime(struct stat *path_stat) { if (path_stat->st_mtime != 0) { return path_stat->st_mtime; } else { return path_stat->st_mtim.tv_sec; // Fallback to st_mtim.tv_sec if st_mtime is zero or invalid } } void *updateIfTopLevelFoldersMtimesChangedThread(void *arg) { char *path = (char *)arg; struct stat path_stat; if (stat(path, &path_stat) == -1) { perror("stat"); pthread_exit(NULL); } if (getModificationTime(&path_stat) > lastTimeAppRan && lastTimeAppRan > 0) { updateLibrary(path); pthread_exit(NULL); } DIR *dir = opendir(path); if (!dir) { perror("opendir"); pthread_exit(NULL); } struct dirent *entry; while ((entry = readdir(dir)) != NULL) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; } char fullPath[1024]; snprintf(fullPath, sizeof(fullPath), "%s/%s", path, entry->d_name); if (stat(fullPath, &path_stat) == -1) { perror("stat"); continue; } if (S_ISDIR(path_stat.st_mode)) { if (getModificationTime(&path_stat) > lastTimeAppRan && lastTimeAppRan > 0) { updateLibrary(path); break; } } } closedir(dir); pthread_exit(NULL); } // This only checks the library mtime and toplevel subfolders mtimes void updateLibraryIfChangedDetected() { pthread_t tid; if (pthread_create(&tid, NULL, updateIfTopLevelFoldersMtimesChangedThread, (void *)settings.path) != 0) { perror("pthread_create"); } } kew-2.8.2/src/playerops.h000066400000000000000000000065151467402032100152750ustar00rootroot00000000000000 #ifndef PLAYEROPS_H #define PLAYEROPS_H #include #include #include #include #include #include #include #include "player.h" #include "songloader.h" #include "settings.h" #include "soundcommon.h" #ifndef CLOCK_MONOTONIC #define CLOCK_MONOTONIC 1 #endif #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif typedef struct { char filePath[MAXPATHLEN]; SongData *songdataA; SongData *songdataB; bool loadA; bool loadingFirstDecoder; pthread_mutex_t mutex; } LoadingThreadData; extern GDBusConnection *connection; extern LoadingThreadData loadingdata; extern double elapsedSeconds; extern double pauseSeconds; extern double totalPauseSeconds; extern struct timespec pause_time; extern volatile bool loadedNextSong; extern bool playlistNeedsUpdate; extern bool nextSongNeedsRebuilding; extern bool enqueuedNeedsUpdate; extern bool waitingForPlaylist; extern bool waitingForNext; extern bool usingSongDataA; extern Node *nextSong; extern Node *songToStartFrom; extern int lastPlayedId; extern bool playingMainPlaylist; extern bool songHasErrors; extern bool doQuit; extern bool loadingFailed; extern volatile bool clearingErrors; extern volatile bool songLoading; extern struct timespec start_time; extern bool skipping; extern bool skipOutOfOrder; extern Node *tryNextSong; extern struct timespec lastInputTime; extern struct timespec lastPlaylistChangeTime; extern bool skipFromStopped; extern bool doNotifyMPRISSwitched; extern UserData userData; SongData *getCurrentSongData(void); void rebuildAndUpdatePlaylist(); Node *getNextSong(); void handleRemove(); void enqueueSongs(FileSystemEntry *entry); void updateLastSongSwitchTime(void); void updateLastInputTime(void); void playbackPause(struct timespec *pause_time); void playbackPlay(double *totalPauseSeconds, double *pauseSeconds); void togglePause(double *totalPauseSeconds, double *pauseSeconds, struct timespec *pause_time); void stop(); void toggleRepeat(void); void toggleShuffle(void); void addToSpecialPlaylist(void); void toggleBlocks(AppSettings *settings); void toggleColors(AppSettings *settings); void toggleVisualizer(AppSettings *settings); void quit(void); void calcElapsedTime(void); Node *getSongByNumber(PlayList *playlist, int songNumber); void skipToNextSong(void); void skipToPrevSong(void); void skipToSong(int id, bool startPlaying); void seekForward(void); void seekBack(void); void skipToNumberedSong(int songNumber); void skipToLastSong(void); void loadSong(Node *song, LoadingThreadData *loadingdata); void loadNext(LoadingThreadData *loadingdata); int loadFirst(Node *song); void flushSeek(void); Node *findSelectedEntryById(PlayList *playlist, int id); void emitSeekedSignal(double newPositionSeconds); void rebuildNextSong(Node *song); void updateLibrary(char *path); void askIfCacheLibrary(); void unloadSongA(); void unloadSongB(); void unloadPreviousSong(); void createLibrary(AppSettings *settings); void loadNextSong(); void setCurrentSongToNext(); void finishLoading(); void resetTimeCount(); bool setPosition(gint64 newPosition); bool seekPosition(gint64 offset); void silentSwitchToNext(bool loadSong); void reshufflePlaylist(); bool determineCurrentSongData(SongData **currentSongData); void updateLibraryIfChangedDetected(); #endif kew-2.8.2/src/playlist.c000066400000000000000000000633501467402032100151130ustar00rootroot00000000000000#define _XOPEN_SOURCE 700 #define __USE_XOPEN_EXTENDED 1 #include "playlist.h" /* playlist.c Playlist related functions. */ #define MAX_SEARCH_SIZE 256 #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif const char PLAYLIST_EXTENSIONS[] = "\\.(m3u)$"; const char mainPlaylistName[] = "kew.m3u"; // The playlist unshuffled as it appears in playlist view PlayList *originalPlaylist = NULL; // The (sometimes shuffled) sequence of songs that will be played PlayList playlist = {NULL, NULL, 0, PTHREAD_MUTEX_INITIALIZER}; // The playlist from kew.m3u PlayList *specialPlaylist = NULL; char search[MAX_SEARCH_SIZE]; char playlistName[MAX_SEARCH_SIZE]; bool shuffle = false; int numDirs = 0; volatile int stopPlaylistDurationThread = 0; Node *currentSong = NULL; int nodeIdCounter = 0; Node *getListNext(Node *node) { return (node == NULL) ? NULL : node->next; } Node *getListPrev(Node *node) { return (node == NULL) ? NULL : node->prev; } void addToList(PlayList *list, Node *newNode) { if (list->count >= MAX_FILES) return; list->count++; if (list->head == NULL) { newNode->prev = NULL; list->head = newNode; list->tail = newNode; } else { newNode->prev = list->tail; list->tail->next = newNode; list->tail = newNode; } } Node *deleteFromList(PlayList *list, Node *node) { if (list->head == NULL || node == NULL) return NULL; if (list->head == node) { list->head = node->next; if (list->head == NULL) { list->tail = NULL; } } if (node == list->tail) list->tail = node->prev; if (node->prev != NULL) node->prev->next = node->next; if (node->next != NULL) node->next->prev = node->prev; if (node->song.filePath != NULL) free(node->song.filePath); Node *nextNode = node->next; free(node); list->count--; return nextNode; } void deletePlaylist(PlayList *list) { if (list == NULL) return; Node *current = list->head; while (current != NULL) { Node *next = current->next; free(current->song.filePath); free(current); current = next; } // Reset the playlist list->head = NULL; list->tail = NULL; list->count = 0; } void shufflePlaylist(PlayList *playlist) { if (playlist == NULL || playlist->count <= 1) { return; // No need to shuffle } // Convert the linked list to an array Node **nodes = (Node **)malloc(playlist->count * sizeof(Node *)); if (nodes == NULL) { printf("Memory allocation error.\n"); exit(0); } Node *current = playlist->head; int i = 0; while (current != NULL) { nodes[i++] = current; current = current->next; } // Shuffle the array using Fisher-Yates algorithm for (int j = playlist->count - 1; j >= 1; --j) { int k = rand() % (j + 1); Node *temp = nodes[j]; nodes[j] = nodes[k]; nodes[k] = temp; } playlist->head = nodes[0]; playlist->tail = nodes[playlist->count - 1]; for (int j = 0; j < playlist->count; ++j) { nodes[j]->next = (j < playlist->count - 1) ? nodes[j + 1] : NULL; nodes[j]->prev = (j > 0) ? nodes[j - 1] : NULL; } free(nodes); } void insertAsFirst(Node *currentSong, PlayList *playlist) { if (currentSong == NULL || playlist == NULL) { return; } if (playlist->head == NULL) { currentSong->next = NULL; currentSong->prev = NULL; playlist->head = currentSong; playlist->tail = currentSong; } else { if (currentSong != playlist->head) { if (currentSong->next != NULL) { currentSong->next->prev = currentSong->prev; } else { playlist->tail = currentSong->prev; } if (currentSong->prev != NULL) { currentSong->prev->next = currentSong->next; } // Add the currentSong as the new head currentSong->next = playlist->head; currentSong->prev = NULL; playlist->head->prev = currentSong; playlist->head = currentSong; } } } void shufflePlaylistStartingFromSong(PlayList *playlist, Node *song) { shufflePlaylist(playlist); if (song != NULL && playlist->count > 1) { insertAsFirst(song, playlist); } } int compare(const struct dirent **a, const struct dirent **b) { const char *nameA = (*a)->d_name; const char *nameB = (*b)->d_name; if (nameA[0] == '_' && nameB[0] != '_') { return -1; } else if (nameA[0] != '_' && nameB[0] == '_') { return 1; } return strcmp(nameA, nameB); } void createNode(Node **node, const char *directoryPath, int id) { SongInfo song; song.filePath = strdup(directoryPath); song.duration = 0.0; *node = (Node *)malloc(sizeof(Node)); if (*node == NULL) { printf("Failed to allocate memory."); exit(0); return; } (*node)->song = song; (*node)->next = NULL; (*node)->prev = NULL; (*node)->id = id; } void buildPlaylistRecursive(const char *directoryPath, const char *allowedExtensions, PlayList *playlist) { int res = isDirectory(directoryPath); if (res != 1 && res != -1 && directoryPath != NULL) { Node *node = NULL; createNode(&node, directoryPath, nodeIdCounter++); addToList(playlist, node); return; } DIR *dir = opendir(directoryPath); if (dir == NULL) { printf("Failed to open directory: %s\n", directoryPath); return; } regex_t regex; int ret = regcomp(®ex, allowedExtensions, REG_EXTENDED); if (ret != 0) { printf("Failed to compile regular expression\n"); closedir(dir); return; } char exto[6]; struct dirent **entries; int numEntries = scandir(directoryPath, &entries, NULL, compare); if (numEntries < 0) { printf("Failed to scan directory: %s\n", directoryPath); return; } for (int i = 0; i < numEntries && playlist->count < MAX_FILES; i++) { struct dirent *entry = entries[i]; if (entry->d_name[0] == '.' || strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; } char filePath[FILENAME_MAX]; snprintf(filePath, sizeof(filePath), "%s/%s", directoryPath, entry->d_name); if (isDirectory(filePath)) { int songCount = playlist->count; buildPlaylistRecursive(filePath, allowedExtensions, playlist); if (playlist->count > songCount) numDirs++; } else { extractExtension(entry->d_name, sizeof(exto) - 1, exto); if (match_regex(®ex, exto) == 0) { snprintf(filePath, sizeof(filePath), "%s/%s", directoryPath, entry->d_name); Node *node = NULL; createNode(&node, filePath, nodeIdCounter++); addToList(playlist, node); } } } for (int i = 0; i < numEntries; i++) { free(entries[i]); } free(entries); closedir(dir); regfree(®ex); } int playDirectory(const char *directoryPath, const char *allowedExtensions, PlayList *playlist) { DIR *dir = opendir(directoryPath); if (dir == NULL) { printf("Failed to open directory: %s\n", directoryPath); return -1; } regex_t regex; int ret = regcomp(®ex, allowedExtensions, REG_EXTENDED); if (ret != 0) { return -1; } char ext[6]; struct dirent *entry; while ((entry = readdir(dir)) != NULL) { extractExtension(entry->d_name, sizeof(ext) - 1, ext); if (match_regex(®ex, ext) == 0) { char filePath[FILENAME_MAX]; snprintf(filePath, sizeof(filePath), "%s/%s", directoryPath, entry->d_name); Node *node = NULL; createNode(&node, filePath, nodeIdCounter++); addToList(playlist, node); } } closedir(dir); return 0; } int joinPlaylist(PlayList *dest, PlayList *src) { if (src->count == 0) { return 0; } if (dest->count == 0) { dest->head = src->head; dest->tail = src->tail; } else { dest->tail->next = src->head; src->head->prev = dest->tail; dest->tail = src->tail; } dest->count += src->count; src->head = NULL; src->tail = NULL; src->count = 0; return 1; } void makePlaylistName(const char *search) { char *duplicateSearch = strdup(search); strcat(playlistName, duplicateSearch); free(duplicateSearch); strcat(playlistName, ".m3u"); int i = 0; while (playlistName[i] != '\0') { if (playlistName[i] == ':') { playlistName[i] = '-'; } i++; } } void readM3UFile(const char *filename, PlayList *playlist) { FILE *file = fopen(filename, "r"); char directory[MAXPATHLEN]; if (file == NULL) { return; } getDirectoryFromPath(filename, directory); char line[MAXPATHLEN]; while (fgets(line, sizeof(line), file)) { size_t len = strcspn(line, "\r\n"); line[len] = '\0'; size_t start = 0; while (isspace(line[start])) { start++; } size_t end = strlen(line); while (end > start && isspace(line[end - 1])) { end--; } line[end] = '\0'; if (line[0] != '#' && line[0] != '\0') { char songPath[MAXPATHLEN]; memset(songPath, '\0', sizeof(songPath)); if (strchr(line, '/') == NULL && strchr(line, '\\') == NULL) strcat(songPath, directory); strcat(songPath, line); Node *newNode = NULL; createNode(&newNode, songPath, nodeIdCounter++); if (playlist->head == NULL) { playlist->head = newNode; playlist->tail = newNode; } else { playlist->tail->next = newNode; newNode->prev = playlist->tail; playlist->tail = newNode; } playlist->count++; } } fclose(file); } int makePlaylist(int argc, char *argv[], bool exactSearch, const char *path) { enum SearchType searchType = SearchAny; int searchTypeIndex = 1; const char *delimiter = ":"; PlayList partialPlaylist = {NULL, NULL, 0, PTHREAD_MUTEX_INITIALIZER}; const char *allowedExtensions = AUDIO_EXTENSIONS; if (strcmp(argv[1], "all") == 0) { searchType = ReturnAllSongs; shuffle = true; } if (argc > 1) { if (strcmp(argv[1], "list") == 0 && argc > 2) { allowedExtensions = PLAYLIST_EXTENSIONS; searchType = SearchPlayList; } if (strcmp(argv[1], "random") == 0 || strcmp(argv[1], "rand") == 0 || strcmp(argv[1], "shuffle") == 0) { int count = 0; while (argv[count] != NULL) { count++; } if (count > 2) { searchTypeIndex = 2; shuffle = true; } } if (strcmp(argv[searchTypeIndex], "dir") == 0) searchType = DirOnly; else if (strcmp(argv[searchTypeIndex], "song") == 0) searchType = FileOnly; } int start = searchTypeIndex + 1; if (searchType == FileOnly || searchType == DirOnly || searchType == SearchPlayList) start = searchTypeIndex + 2; search[0] = '\0'; for (int i = start - 1; i < argc; i++) { strcat(search, " "); strcat(search, argv[i]); } makePlaylistName(search); if (strstr(search, delimiter)) { shuffle = true; } if (searchType == ReturnAllSongs) { pthread_mutex_lock(&(playlist.mutex)); buildPlaylistRecursive(path, allowedExtensions, &playlist); pthread_mutex_unlock(&(playlist.mutex)); } else { char *token = strtok(search, delimiter); while (token != NULL) { char buf[MAXPATHLEN] = {0}; if (strncmp(token, "song", 4) == 0) { memmove(token, token + 4, strlen(token + 4) + 1); searchType = FileOnly; } trim(token); if (walker(path, token, buf, allowedExtensions, searchType, exactSearch) == 0) { if (strcmp(argv[1], "list") == 0) { readM3UFile(buf, &playlist); } else { pthread_mutex_lock(&(playlist.mutex)); buildPlaylistRecursive(buf, allowedExtensions, &partialPlaylist); joinPlaylist(&playlist, &partialPlaylist); pthread_mutex_unlock(&(playlist.mutex)); } } token = strtok(NULL, delimiter); } } if (numDirs > 1) shuffle = true; if (shuffle) shufflePlaylist(&playlist); if (playlist.head == NULL) printf("Music not found\n"); return 0; } void generateM3UFilename(const char *basePath, const char *filePath, char *m3uFilename, size_t size) { const char *baseName = strrchr(filePath, '/'); if (baseName == NULL) { baseName = filePath; // No '/' found, use the entire filename } else { baseName++; // Skip the '/' character } const char *dot = strrchr(baseName, '.'); if (dot == NULL) { // No '.' found, copy the base name and append ".m3u" if (basePath[strlen(basePath) - 1] == '/') { snprintf(m3uFilename, size, "%s%s.m3u", basePath, baseName); } else { snprintf(m3uFilename, size, "%s/%s.m3u", basePath, baseName); } } else { // Copy the base name up to the dot and append ".m3u" size_t baseNameLen = dot - baseName; if (basePath[strlen(basePath) - 1] == '/') { snprintf(m3uFilename, size, "%s%.*s.m3u", basePath, (int)baseNameLen, baseName); } else { snprintf(m3uFilename, size, "%s/%.*s.m3u", basePath, (int)baseNameLen, baseName); } } } void writeM3UFile(const char *filename, PlayList *playlist) { FILE *file = fopen(filename, "w"); if (file == NULL) { return; } Node *currentNode = playlist->head; while (currentNode != NULL) { fprintf(file, "%s\n", currentNode->song.filePath); currentNode = currentNode->next; } fclose(file); } void loadSpecialPlaylist(const char *directory) { char playlistPath[MAXPATHLEN]; c_strcpy(playlistPath, sizeof(playlistPath), directory); if (playlistPath[strlen(playlistPath) - 1] != '/') strcat(playlistPath, "/"); strcat(playlistPath, mainPlaylistName); specialPlaylist = malloc(sizeof(PlayList)); if (specialPlaylist == NULL) { printf("Failed to allocate memory for special playlist.\n"); exit(0); } specialPlaylist->count = 0; specialPlaylist->head = NULL; specialPlaylist->tail = NULL; readM3UFile(playlistPath, specialPlaylist); } void saveSpecialPlaylist(const char *directory) { if (directory == NULL) { return; } char playlistPath[MAXPATHLEN]; playlistPath[0] = '\0'; c_strcpy(playlistPath, sizeof(playlistPath), directory); if (playlistPath[0] == '\0') { return; } if (playlistPath[strlen(playlistPath) - 1] != '/') strcat(playlistPath, "/"); strcat(playlistPath, mainPlaylistName); if (specialPlaylist != NULL && specialPlaylist->count > 0) writeM3UFile(playlistPath, specialPlaylist); } void savePlaylist(const char *path) { if (path == NULL) { return; } if (playlist.head == NULL || playlist.head->song.filePath == NULL) return; char m3uFilename[MAXPATHLEN]; generateM3UFilename(path, playlist.head->song.filePath, m3uFilename, sizeof(m3uFilename)); writeM3UFile(m3uFilename, &playlist); } Node *deepCopyNode(Node *originalNode) { if (originalNode == NULL) { return NULL; } Node *newNode = malloc(sizeof(Node)); if (newNode == NULL) { return NULL; } newNode->song.filePath = strdup(originalNode->song.filePath); newNode->song.duration = originalNode->song.duration; newNode->prev = NULL; newNode->id = originalNode->id; newNode->next = deepCopyNode(originalNode->next); if (newNode->next != NULL) { newNode->next->prev = newNode; } return newNode; } Node *findTail(Node *head) { if (head == NULL) return NULL; Node *current = head; while (current->next != NULL) { current = current->next; } return current; } PlayList deepCopyPlayList(PlayList *originalList) { PlayList newList = {NULL, NULL, 0, PTHREAD_MUTEX_INITIALIZER}; deepCopyPlayListOntoList(originalList, &newList); return newList; } void deepCopyPlayListOntoList(PlayList *originalList, PlayList *newList) { if (originalList == NULL) { return; } newList->head = deepCopyNode(originalList->head); newList->tail = findTail(newList->head); newList->count = originalList->count; } Node *findPathInPlaylist(char *path, PlayList *playlist) { Node *currentNode = playlist->head; while (currentNode != NULL) { if (strcmp(currentNode->song.filePath, path) == 0) { return currentNode; } currentNode = currentNode->next; } return NULL; } Node *findLastPathInPlaylist(char *path, PlayList *playlist) { Node *currentNode = playlist->tail; while (currentNode != NULL) { if (strcmp(currentNode->song.filePath, path) == 0) { return currentNode; } currentNode = currentNode->prev; } return NULL; } int findNodeInList(PlayList *list, int id, Node **foundNode) { Node *node = list->head; int row = 0; while (node != NULL) { if (id == node->id) { *foundNode = node; return row; } node = node->next; row++; } *foundNode = NULL; return -1; } void addSongToPlayList(PlayList *list, const char *filePath, int playlistMax) { if (list->count >= playlistMax) return; Node *newNode = NULL; createNode(&newNode, filePath, list->count); addToList(list, newNode); } void traverseFileSystemEntry(FileSystemEntry *entry, PlayList *list, int playlistMax) { if (entry == NULL || list->count >= playlistMax) return; if (entry->isDirectory == 0) { addSongToPlayList(list, entry->fullPath, playlistMax); } if (entry->isDirectory == 1 && entry->children != NULL) { traverseFileSystemEntry(entry->children, list, playlistMax); } if (entry->next != NULL) { traverseFileSystemEntry(entry->next, list, playlistMax); } } void createPlayListFromFileSystemEntry(FileSystemEntry *root, PlayList *list, int playlistMax) { traverseFileSystemEntry(root, list, playlistMax); } int isMusicFile(const char *filename) { const char *extensions[] = {".m4a", ".aac", ".mp3", ".ogg", ".flac",".wav", ".opus"}; size_t numExtensions = sizeof(extensions) / sizeof(extensions[0]); for (size_t i = 0; i < numExtensions; i++) { if (strstr(filename, extensions[i]) != NULL) { return 1; } } return 0; } int containsMusicFiles(FileSystemEntry *entry) { if (entry == NULL) return 0; FileSystemEntry *child = entry->children; while (child != NULL) { if (!child->isDirectory && isMusicFile(child->name)) { return 1; } child = child->next; } return 0; } void addAlbumToPlayList(PlayList *list, FileSystemEntry *album, int playlistMax) { FileSystemEntry *entry = album->children; while (entry != NULL && list->count < playlistMax) { if (!entry->isDirectory && isMusicFile(entry->name)) { addSongToPlayList(list, entry->fullPath, playlistMax); } entry = entry->next; } } void addAlbumsToPlayList(FileSystemEntry *entry, PlayList *list, int playlistMax) { if (entry == NULL || list->count >= playlistMax) return; if (entry->isDirectory && containsMusicFiles(entry)) { addAlbumToPlayList(list, entry, playlistMax); } if (entry->isDirectory && entry->children != NULL) { addAlbumsToPlayList(entry->children, list, playlistMax); } if (entry->next != NULL) { addAlbumsToPlayList(entry->next, list, playlistMax); } } void shuffleEntries(FileSystemEntry **array, size_t n) { if (n > 1) { size_t i; for (i = 0; i < n - 1; i++) { size_t j = i + rand() / (RAND_MAX / (n - i) + 1); FileSystemEntry *temp = array[i]; array[i] = array[j]; array[j] = temp; } } } void collectAlbums(FileSystemEntry *entry, FileSystemEntry **albums, size_t *count) { if (entry == NULL) return; if (entry->isDirectory && containsMusicFiles(entry)) { albums[*count] = entry; (*count)++; } if (entry->isDirectory && entry->children != NULL) { collectAlbums(entry->children, albums, count); } if (entry->next != NULL) { collectAlbums(entry->next, albums, count); } } void addShuffledAlbumsToPlayList(FileSystemEntry *root, PlayList *list, int playlistMax) { size_t maxAlbums = 2000; FileSystemEntry *albums[maxAlbums]; size_t albumCount = 0; collectAlbums(root, albums, &albumCount); srand(time(NULL)); shuffleEntries(albums, albumCount); for (size_t i = 0; i < albumCount && list->count < playlistMax; i++) { addAlbumToPlayList(list, albums[i], playlistMax); } } kew-2.8.2/src/playlist.h000066400000000000000000000037741467402032100151240ustar00rootroot00000000000000#ifndef _DEFAULT_SOURCE #define _DEFAULT_SOURCE #endif #ifndef __USE_XOPEN_EXTENDED #define __USE_XOPEN_EXTENDED #endif #include #include #include #include #include #include "directorytree.h" #include "file.h" #define MAX_FILES 10000 #ifndef PLAYLIST_STRUCT #define PLAYLIST_STRUCT typedef struct { char *filePath; double duration; } SongInfo; typedef struct Node { int id; SongInfo song; struct Node *next; struct Node *prev; } Node; typedef struct { Node *head; Node *tail; int count; pthread_mutex_t mutex; } PlayList; extern Node *currentSong; #endif extern PlayList playlist; extern PlayList *specialPlaylist; extern PlayList *originalPlaylist; extern int nodeIdCounter; Node *getListNext(Node *node); Node *getListPrev(Node *node); void createNode(Node **node, const char *directoryPath, int id); void addToList(PlayList *list, Node *newNode); Node *deleteFromList(PlayList *list, Node *node); void deletePlaylist(PlayList *playlist); void shufflePlaylist(PlayList *playlist); void shufflePlaylistStartingFromSong(PlayList *playlist, Node *song); int makePlaylist(int argc, char *argv[], bool exactSearch, const char *path); void writeCurrentPlaylistToM3UFile(PlayList *playlist); void writeM3UFile(const char *filename, PlayList *playlist); void loadSpecialPlaylist(const char *directory); void saveSpecialPlaylist(const char *directory); void savePlaylist(const char *path); PlayList deepCopyPlayList(PlayList *originalList); void deepCopyPlayListOntoList(PlayList *originalList, PlayList *newList); Node *findPathInPlaylist(char *path, PlayList *playlist); Node *findLastPathInPlaylist(char *path, PlayList *playlist); int findNodeInList(PlayList *list, int id, Node **foundNode); void createPlayListFromFileSystemEntry(FileSystemEntry *root, PlayList *list, int playlistMax); void addShuffledAlbumsToPlayList(FileSystemEntry *root, PlayList *list, int playlistMax); kew-2.8.2/src/playlist_ui.c000066400000000000000000000124011467402032100155770ustar00rootroot00000000000000#include "playlist_ui.h" int startIter = 0; void getTerminalSize(int *width, int *height) { getTermSize(width, height); } Node *determineStartNode(Node *head, int *foundAt, bool *startFromCurrent, int listSize) { Node *node = head; Node *foundNode = NULL; int numSongs = 0; *foundAt = -1; while (node != NULL && numSongs <= listSize) { if (currentSong != NULL && currentSong->id == node->id) { *foundAt = numSongs; foundNode = node; break; } node = node->next; numSongs++; } *startFromCurrent = (*foundAt > -1) ? true : false; return foundNode ? foundNode : head; } void preparePlaylistString(Node *node, char *buffer, int bufferSize, int shortenAmount) { if (node == NULL || buffer == NULL) { buffer[0] = '\0'; return; } char filePath[MAXPATHLEN]; c_strcpy(filePath, sizeof(filePath), node->song.filePath); char *lastSlash = strrchr(filePath, '/'); char *lastDot = strrchr(filePath, '.'); if (lastSlash != NULL && lastDot != NULL && lastDot > lastSlash) { int nameLength = lastDot - lastSlash - 1; nameLength = (nameLength < bufferSize - 1) ? nameLength : bufferSize - 1; strncpy(buffer, lastSlash + 1, nameLength); buffer[nameLength] = '\0'; removeUnneededChars(buffer); shortenString(buffer, shortenAmount); trim(buffer); } else { buffer[0] = '\0'; } } int displayPlaylistItems(Node *startNode, int startIter, int maxListSize, int termWidth, int indent, int chosenSong, int *chosenNodeId) { int numPrintedRows = 0; Node *node = startNode; char *buffer = (char *)malloc(MAXPATHLEN * sizeof(char)); if (!buffer) { return 0; } for (int i = startIter; node != NULL && i < startIter + maxListSize; i++) { preparePlaylistString(node, buffer, MAXPATHLEN, termWidth - indent - 10); if (buffer[0] != '\0') { if (useProfileColors) setTextColor(artistColor); else setColor(); printBlankSpaces(indent); printf(" %d. ", i + 1); setDefaultTextColor(); if (i == chosenSong) { *chosenNodeId = node->id; printf("\x1b[7m"); } if (currentSong != NULL && currentSong->id == node->id) { printf("\e[1m\e[39m"); } if (i + 1 < 10) printf(" "); printf("%s \n", buffer); numPrintedRows++; } node = node->next; } free(buffer); return numPrintedRows; } int displayPlaylist(PlayList *list, int maxListSize, int indent, int *chosenSong, int *chosenNodeId, bool reset) { int termWidth, termHeight; getTerminalSize(&termWidth, &termHeight); int foundAt = -1; bool startFromCurrent = false; Node *startNode = determineStartNode(list->head, &foundAt, &startFromCurrent, list->count); // Determine chosen song if (*chosenSong >= list->count) { *chosenSong = list->count - 1; } if (*chosenSong < 0) { *chosenSong = 0; } int startIter = 0; // Determine where to start iterating startIter = (startFromCurrent && (foundAt < startIter || foundAt > startIter + maxListSize)) ? foundAt : startIter; if (*chosenSong < startIter) { startIter = *chosenSong; } if (*chosenSong > startIter + maxListSize - round(maxListSize / 2)) { startIter = *chosenSong - maxListSize + round(maxListSize / 2); } if (reset && !audioData.endOfListReached) { startIter = *chosenSong = foundAt; } // Go up to find the starting node for (int i = foundAt; i > startIter; i--) { if (i > 0 && startNode->prev != NULL) startNode = startNode->prev; } // Go down to adjust the startNode for (int i = (foundAt == -1) ? 0 : foundAt; i < startIter; i++) { if (startNode->next != NULL) startNode = startNode->next; } int printedRows = displayPlaylistItems(startNode, startIter, maxListSize, termWidth, indent, *chosenSong, chosenNodeId); if (printedRows > 1) { while (printedRows < maxListSize) { printf("\n"); printedRows++; } } return printedRows; } kew-2.8.2/src/playlist_ui.h000066400000000000000000000004221467402032100156040ustar00rootroot00000000000000#ifndef PLAYLIST_UI_H #define PLAYLIST_UI_H #include "common_ui.h" #include "playlist.h" #include "songloader.h" #include "term.h" #include "utils.h" int displayPlaylist(PlayList *list, int maxListSize, int indent, int *chosenSong, int *chosenNodeId, bool reset); #endif kew-2.8.2/src/search_ui.c000066400000000000000000000201421467402032100152040ustar00rootroot00000000000000#include "search_ui.h" #define MAX_SEARCH_LEN 32 int numSearchLetters = 0; int numSearchBytes = 0; typedef struct SearchResult { FileSystemEntry *entry; int distance; } SearchResult; // Global variables to store results SearchResult *results = NULL; size_t resultsCount = 0; size_t resultsCapacity = 0; bool newUndisplayedSearch = false; int minSearchLetters = 1; FileSystemEntry *currentSearchEntry = NULL; char searchText[MAX_SEARCH_LEN * 4 + 1]; // unicode can be 4 characters FileSystemEntry *getCurrentSearchEntry() { return currentSearchEntry; } int getSearchResultsCount() { return resultsCount; } // Function to add a result to the global array void addResult(FileSystemEntry *entry, int distance) { if (resultsCount >= resultsCapacity) { resultsCapacity = resultsCapacity == 0 ? 10 : resultsCapacity * 2; results = realloc(results, resultsCapacity * sizeof(SearchResult)); } results[resultsCount].distance = distance; results[resultsCount].entry = entry; resultsCount++; } // Callback function to collect results void collectResult(FileSystemEntry *entry, int distance) { addResult(entry, distance); } // Free allocated memory from previous search void freeSearchResults() { if (results != NULL) { free(results); results = NULL; } resultsCapacity = 0; resultsCount = 0; } void fuzzySearch(FileSystemEntry *root, int threshold) { freeSearchResults(); if (numSearchLetters > minSearchLetters) { fuzzySearchRecursive(root, searchText, threshold, collectResult); } newUndisplayedSearch = true; } int compareResults(const void *a, const void *b) { SearchResult *resultA = (SearchResult *)a; SearchResult *resultB = (SearchResult *)b; return resultA->distance - resultB->distance; } void sortResults() { qsort(results, resultsCount, sizeof(SearchResult), compareResults); } int displaySearchBox(int indent) { printBlankSpaces(indent); printf(" [Search]: "); setDefaultTextColor(); // Save cursor position printf("%s", searchText); printf("\033[s"); printf("█\n"); return 0; } int addToSearchText(const char *str) { if (str == NULL) { return -1; } size_t len = strlen(str); // Check if the string can fit into the search text buffer if (numSearchLetters + 1 > MAX_SEARCH_LEN) { return 0; // Not enough space } // Restore cursor position printf("\033[u"); // Print the string printf("%s", str); // Save cursor position printf("\033[s"); printf("█\n"); // Add the string to the search text buffer for (size_t i = 0; i < len; i++) { searchText[numSearchBytes++] = str[i]; } searchText[numSearchBytes + 1] = '\0'; // Null-terminate the buffer numSearchLetters++; return 0; } // Determine the number of bytes in the last UTF-8 character int getLastCharBytes(const char *str, int len) { if (len == 0) return 0; int i = len - 1; while (i >= 0 && (str[i] & 0xC0) == 0x80) { i--; } return len - i; } // Remove the preceding character from the search text int removeFromSearchText() { if (numSearchLetters == 0) return 0; // Determine the number of bytes to remove for the last character int lastCharBytes = getLastCharBytes(searchText, numSearchBytes); if (lastCharBytes == 0) return 0; // Restore cursor position printf("\033[u"); // Move cursor back one step printf("\033[D"); // Overwrite the character with spaces for (int i = 0; i < lastCharBytes; i++) { printf(" "); } // Move cursor back again to the original position for (int i = 0; i < lastCharBytes; i++) { printf("\033[D"); } // Save cursor position printf("\033[s"); // Print a block character to represent the cursor printf("█"); // Clear the end of the line printf("\033[K"); fflush(stdout); // Remove the character from the buffer numSearchBytes -= lastCharBytes; searchText[numSearchBytes] = '\0'; numSearchLetters--; return 0; } int displaySearchResults(int maxListSize, int indent, int *chosenRow, int startSearchIter) { int term_w, term_h; getTermSize(&term_w, &term_h); int maxNameWidth = term_w - indent - 5; char name[maxNameWidth + 1]; sortResults(); if (*chosenRow >= (int)resultsCount - 1) { *chosenRow = resultsCount - 1; } if (startSearchIter < 0) startSearchIter = 0; if (*chosenRow > startSearchIter + round(maxListSize / 2)) { startSearchIter = *chosenRow - round(maxListSize / 2) + 1; } if (*chosenRow < startSearchIter) startSearchIter = *chosenRow; if (*chosenRow < 0) startSearchIter = *chosenRow = 0; printf("\n"); // Print the sorted results for (size_t i = startSearchIter; i < resultsCount; i++) { if (numSearchLetters < minSearchLetters) break; if ((int)i >= (maxListSize + startSearchIter)) break; setDefaultTextColor(); printBlankSpaces(indent); if (*chosenRow == (int)i) { currentSearchEntry = results[i].entry; if (results[i].entry->isEnqueued) { if (useProfileColors) setTextColor(enqueuedColor); else setColor(); printf("\x1b[7m * "); } else { printf(" \x1b[7m "); } } else { if (results[i].entry->isEnqueued) { if (useProfileColors) setTextColor(enqueuedColor); else setColor(); printf(" * "); } else printf(" "); } name[0] = '\0'; if (results[i].entry->isDirectory) { if (results[i].entry->parent != NULL && strcmp(results[i].entry->parent->name, "root") != 0) snprintf(name, maxNameWidth + 1, "[%s] (%s)", results[i].entry->name, results[i].entry->parent->name); else snprintf(name, maxNameWidth + 1, "[%s]", results[i].entry->name); } else { if (results[i].entry->parent != NULL && strcmp(results[i].entry->parent->name, "root") != 0) snprintf(name, maxNameWidth + 1, "%s (%s)", results[i].entry->name, results[i].entry->parent->name); else snprintf(name, maxNameWidth + 1, "%s", results[i].entry->name); } printf("%s\n", name); } return 0; } int displaySearch(int maxListSize, int indent, int *chosenRow, int startSearchIter) { displaySearchBox(indent); displaySearchResults(maxListSize, indent, chosenRow, startSearchIter); return 0; } kew-2.8.2/src/search_ui.h000066400000000000000000000006771467402032100152240ustar00rootroot00000000000000#include #include #include "directorytree.h" #include "term.h" #include "common_ui.h" extern bool newUndisplayedSearch; int displaySearch(int maxListSize, int indent, int *chosenRow, int startSearchIter); int addToSearchText(const char *str); int removeFromSearchText(); int getSearchResultsCount(); void fuzzySearch(FileSystemEntry *root, int threshold); void freeSearchResults(); FileSystemEntry *getCurrentSearchEntry(); kew-2.8.2/src/settings.c000066400000000000000000000624071467402032100151140ustar00rootroot00000000000000#include "settings.h" /* settings.c Functions related to the config file. */ #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif const char SETTINGS_FILE[] = "kewrc"; time_t lastTimeAppRan; void freeKeyValuePairs(KeyValuePair *pairs, int count) { for (int i = 0; i < count; i++) { free(pairs[i].key); free(pairs[i].value); } free(pairs); } AppSettings constructAppSettings(KeyValuePair *pairs, int count) { AppSettings settings; memset(&settings, 0, sizeof(settings)); strncpy(settings.coverEnabled, "1", sizeof(settings.coverEnabled)); strncpy(settings.allowNotifications, "1", sizeof(settings.allowNotifications)); strncpy(settings.coverAnsi, "0", sizeof(settings.coverAnsi)); strncpy(settings.visualizerEnabled, "1", sizeof(settings.visualizerEnabled)); strncpy(settings.useProfileColors, "0", sizeof(settings.useProfileColors)); strncpy(settings.hideLogo, "0", sizeof(settings.hideLogo)); strncpy(settings.hideHelp, "0", sizeof(settings.hideHelp)); strncpy(settings.cacheLibrary, "-1", sizeof(settings.cacheLibrary)); strncpy(settings.tabNext, "\t", sizeof(settings.tabNext)); strncpy(settings.volumeUp, "+", sizeof(settings.volumeUp)); strncpy(settings.volumeUpAlt, "=", sizeof(settings.volumeUpAlt)); strncpy(settings.volumeDown, "-", sizeof(settings.volumeDown)); strncpy(settings.previousTrackAlt, "h", sizeof(settings.previousTrackAlt)); strncpy(settings.nextTrackAlt, "l", sizeof(settings.nextTrackAlt)); strncpy(settings.scrollUpAlt, "k", sizeof(settings.scrollUpAlt)); strncpy(settings.scrollDownAlt, "j", sizeof(settings.scrollDownAlt)); strncpy(settings.toggleColorsDerivedFrom, "i", sizeof(settings.toggleColorsDerivedFrom)); strncpy(settings.toggleVisualizer, "v", sizeof(settings.toggleVisualizer)); strncpy(settings.toggleAscii, "b", sizeof(settings.toggleAscii)); strncpy(settings.toggleRepeat, "r", sizeof(settings.toggleRepeat)); strncpy(settings.toggleShuffle, "s", sizeof(settings.toggleShuffle)); strncpy(settings.togglePause, "p", sizeof(settings.togglePause)); strncpy(settings.seekBackward, "a", sizeof(settings.seekBackward)); strncpy(settings.seekForward, "d", sizeof(settings.seekForward)); strncpy(settings.savePlaylist, "x", sizeof(settings.savePlaylist)); strncpy(settings.updateLibrary, "u", sizeof(settings.updateLibrary)); strncpy(settings.addToMainPlaylist, ".", sizeof(settings.addToMainPlaylist)); strncpy(settings.hardPlayPause, " ", sizeof(settings.hardPlayPause)); strncpy(settings.hardSwitchNumberedSong, "\n", sizeof(settings.hardSwitchNumberedSong)); strncpy(settings.hardPrev, "[D", sizeof(settings.hardPrev)); strncpy(settings.hardNext, "[C", sizeof(settings.hardNext)); strncpy(settings.hardScrollUp, "[A", sizeof(settings.hardScrollUp)); strncpy(settings.hardScrollDown, "[B", sizeof(settings.hardScrollDown)); strncpy(settings.hardShowPlaylist, "OQ", sizeof(settings.hardShowPlaylist)); strncpy(settings.hardShowPlaylistAlt, "[[B", sizeof(settings.hardShowPlaylistAlt)); strncpy(settings.showPlaylistAlt, "", sizeof(settings.showPlaylistAlt)); strncpy(settings.hardShowKeys, "[17~", sizeof(settings.hardShowKeys)); strncpy(settings.hardShowKeysAlt, "[17~", sizeof(settings.hardShowKeysAlt)); strncpy(settings.showKeysAlt, "", sizeof(settings.showKeysAlt)); strncpy(settings.hardShowTrack, "OS", sizeof(settings.hardShowTrack)); strncpy(settings.hardShowTrackAlt, "[[D", sizeof(settings.hardShowTrackAlt)); strncpy(settings.showTrackAlt, "", sizeof(settings.showTrackAlt)); strncpy(settings.hardEndOfPlaylist, "G", sizeof(settings.hardEndOfPlaylist)); strncpy(settings.hardShowLibrary, "OR", sizeof(settings.hardShowLibrary)); strncpy(settings.hardShowLibraryAlt, "[[C", sizeof(settings.hardShowLibraryAlt)); strncpy(settings.showLibraryAlt, "", sizeof(settings.showLibraryAlt)); strncpy(settings.hardShowSearch, "[15~", sizeof(settings.hardShowSearch)); strncpy(settings.hardShowSearchAlt, "[[E", sizeof(settings.hardShowSearchAlt)); strncpy(settings.showSearchAlt, "", sizeof(settings.showSearchAlt)); strncpy(settings.hardNextPage, "[6~", sizeof(settings.hardNextPage)); strncpy(settings.hardPrevPage, "[5~", sizeof(settings.hardPrevPage)); strncpy(settings.hardRemove, "[3~", sizeof(settings.hardRemove)); strncpy(settings.hardRemove2, "[P", sizeof(settings.hardRemove2)); strncpy(settings.lastVolume, "100", sizeof(settings.lastVolume)); strncpy(settings.color, "6", sizeof(settings.color)); strncpy(settings.artistColor, "6", sizeof(settings.artistColor)); strncpy(settings.titleColor, "6", sizeof(settings.titleColor)); strncpy(settings.enqueuedColor, "7", sizeof(settings.enqueuedColor)); strncpy(settings.quit, "q", sizeof(settings.quit)); strncpy(settings.hardQuit, "\x1B", sizeof(settings.hardQuit)); if (pairs == NULL) { return settings; } for (int i = 0; i < count; i++) { KeyValuePair *pair = &pairs[i]; if (strcmp(stringToLower(pair->key), "path") == 0) { snprintf(settings.path, sizeof(settings.path), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "coverenabled") == 0) { snprintf(settings.coverEnabled, sizeof(settings.coverEnabled), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "coveransi") == 0) { snprintf(settings.coverAnsi, sizeof(settings.coverAnsi), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "visualizerenabled") == 0) { snprintf(settings.visualizerEnabled, sizeof(settings.visualizerEnabled), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "useprofilecolors") == 0) { snprintf(settings.useProfileColors, sizeof(settings.useProfileColors), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "visualizerheight") == 0) { snprintf(settings.visualizerHeight, sizeof(settings.visualizerHeight), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "volumeup") == 0) { snprintf(settings.volumeUp, sizeof(settings.volumeUp), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "volumeupalt") == 0) { snprintf(settings.volumeUpAlt, sizeof(settings.volumeUpAlt), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "volumedown") == 0) { snprintf(settings.volumeDown, sizeof(settings.volumeDown), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "previoustrackalt") == 0) { snprintf(settings.previousTrackAlt, sizeof(settings.previousTrackAlt), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "nexttrackalt") == 0) { snprintf(settings.nextTrackAlt, sizeof(settings.nextTrackAlt), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "scrollupalt") == 0) { snprintf(settings.scrollUpAlt, sizeof(settings.scrollUpAlt), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "scrolldownalt") == 0) { snprintf(settings.scrollDownAlt, sizeof(settings.scrollDownAlt), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "switchnumberedsong") == 0) { snprintf(settings.switchNumberedSong, sizeof(settings.switchNumberedSong), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "togglepause") == 0) { snprintf(settings.togglePause, sizeof(settings.togglePause), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "togglecolorsderivedfrom") == 0) { snprintf(settings.toggleColorsDerivedFrom, sizeof(settings.toggleColorsDerivedFrom), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "togglevisualizer") == 0) { snprintf(settings.toggleVisualizer, sizeof(settings.toggleVisualizer), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "toggleascii") == 0) { snprintf(settings.toggleAscii, sizeof(settings.toggleAscii), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "togglerepeat") == 0) { snprintf(settings.toggleRepeat, sizeof(settings.toggleRepeat), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "toggleshuffle") == 0) { snprintf(settings.toggleShuffle, sizeof(settings.toggleShuffle), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "seekbackward") == 0) { snprintf(settings.seekBackward, sizeof(settings.seekBackward), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "seekforward") == 0) { snprintf(settings.seekForward, sizeof(settings.seekForward), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "saveplaylist") == 0) { snprintf(settings.savePlaylist, sizeof(settings.savePlaylist), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "addtomainplaylist") == 0) { snprintf(settings.quit, sizeof(settings.quit), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "lastvolume") == 0) { snprintf(settings.lastVolume, sizeof(settings.lastVolume), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "allownotifications") == 0) { snprintf(settings.allowNotifications, sizeof(settings.allowNotifications), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "color") == 0) { snprintf(settings.color, sizeof(settings.color), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "artistcolor") == 0) { snprintf(settings.artistColor, sizeof(settings.artistColor), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "enqueuedcolor") == 0) { snprintf(settings.enqueuedColor, sizeof(settings.enqueuedColor), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "titlecolor") == 0) { snprintf(settings.titleColor, sizeof(settings.titleColor), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "hidelogo") == 0) { snprintf(settings.hideLogo, sizeof(settings.hideLogo), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "hidehelp") == 0) { snprintf(settings.hideHelp, sizeof(settings.hideHelp), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "cachelibrary") == 0) { snprintf(settings.cacheLibrary, sizeof(settings.cacheLibrary), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "quit") == 0) { snprintf(settings.quit, sizeof(settings.quit), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "updatelibrary") == 0) { snprintf(settings.updateLibrary, sizeof(settings.updateLibrary), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "showplaylistalt") == 0) { snprintf(settings.showPlaylistAlt, sizeof(settings.showPlaylistAlt), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "showlibraryalt") == 0) { snprintf(settings.showLibraryAlt, sizeof(settings.showLibraryAlt), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "showtrackalt") == 0) { snprintf(settings.showTrackAlt, sizeof(settings.showTrackAlt), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "showsearchalt") == 0) { snprintf(settings.showSearchAlt, sizeof(settings.showSearchAlt), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "showkeysalt") == 0) { snprintf(settings.showKeysAlt, sizeof(settings.showKeysAlt), "%s", pair->value); } } freeKeyValuePairs(pairs, count); return settings; } KeyValuePair *readKeyValuePairs(const char *file_path, int *count, time_t *lastTimeAppRan) { FILE *file = fopen(file_path, "r"); if (file == NULL) { return NULL; } struct stat file_stat; if (stat(file_path, &file_stat) == -1) { perror("stat"); return NULL; } // Save the modification time (mtime) of the file *lastTimeAppRan = (file_stat.st_mtime > 0) ? file_stat.st_mtime : file_stat.st_mtim.tv_sec; KeyValuePair *pairs = NULL; int pair_count = 0; char line[256]; while (fgets(line, sizeof(line), file)) { // Remove trailing newline character if present line[strcspn(line, "\n")] = '\0'; char *delimiter = strchr(line, '='); if (delimiter != NULL) { *delimiter = '\0'; char *value = delimiter + 1; pair_count++; pairs = realloc(pairs, pair_count * sizeof(KeyValuePair)); KeyValuePair *current_pair = &pairs[pair_count - 1]; current_pair->key = strdup(line); current_pair->value = strdup(value); } } fclose(file); *count = pair_count; return pairs; } const char *getDefaultMusicFolder() { const char *home = getHomePath(); if (home != NULL) { static char musicPath[MAXPATHLEN]; snprintf(musicPath, sizeof(musicPath), "%s/Music", home); return musicPath; } else { return NULL; // Return NULL if XDG home is not found. } } int getMusicLibraryPath(char *path) { char expandedPath[MAXPATHLEN]; if (path[0] != '\0' && path[0] != '\r') { if (expandPath(path, expandedPath) >= 0) strcpy(path, expandedPath); } return 0; } void getConfig(AppSettings *settings) { int pair_count; char *configdir = getConfigPath(); char *filepath = NULL; // Create the directory if it doesn't exist struct stat st = {0}; if (stat(configdir, &st) == -1) { if (mkdir(configdir, 0700) != 0) { perror("mkdir"); exit(EXIT_FAILURE); } } size_t filepath_length = strlen(configdir) + strlen("/") + strlen(SETTINGS_FILE) + 1; filepath = (char *)malloc(filepath_length); strcpy(filepath, configdir); strcat(filepath, "/"); strcat(filepath, SETTINGS_FILE); KeyValuePair *pairs = readKeyValuePairs(filepath, &pair_count, &lastTimeAppRan); free(filepath); *settings = constructAppSettings(pairs, pair_count); allowNotifications = (settings->allowNotifications[0] == '1'); coverEnabled = (settings->coverEnabled[0] == '1'); coverAnsi = (settings->coverAnsi[0] == '1'); visualizerEnabled = (settings->visualizerEnabled[0] == '1'); useProfileColors = (settings->useProfileColors[0] == '1'); hideLogo = (settings->hideLogo[0] == '1'); hideHelp = (settings->hideHelp[0] == '1'); int temp = atoi(settings->color); if (temp >= 0) mainColor = temp; temp = atoi(settings->artistColor); if (temp >= 0) artistColor = temp; temp = atoi(settings->enqueuedColor); if (temp >= 0) enqueuedColor = temp; temp = atoi(settings->titleColor); if (temp >= 0) titleColor = temp; int temp2 = atoi(settings->visualizerHeight); if (temp2 > 0) visualizerHeight = temp2; int temp3 = atoi(settings->lastVolume); if (temp3 >= 0) setVolume(temp3); int temp4 = atoi(settings->cacheLibrary); if (temp4 >= 0) cacheLibrary = temp4; getMusicLibraryPath(settings->path); free(configdir); } void setConfig(AppSettings *settings) { // Create the file path char *configdir = getConfigPath(); char *filepath = (char *)malloc(strlen(configdir) + strlen("/") + strlen(SETTINGS_FILE) + 1); strcpy(filepath, configdir); strcat(filepath, "/"); strcat(filepath, SETTINGS_FILE); FILE *file = fopen(filepath, "w"); if (file == NULL) { fprintf(stderr, "Error opening file: %s\n", filepath); free(filepath); free(configdir); return; } if (settings->allowNotifications[0] == '\0') allowNotifications ? c_strcpy(settings->allowNotifications, sizeof(settings->allowNotifications), "1") : c_strcpy(settings->allowNotifications, sizeof(settings->allowNotifications), "0"); if (settings->coverEnabled[0] == '\0') coverEnabled ? c_strcpy(settings->coverEnabled, sizeof(settings->coverEnabled), "1") : c_strcpy(settings->coverEnabled, sizeof(settings->coverEnabled), "0"); if (settings->coverAnsi[0] == '\0') coverAnsi ? c_strcpy(settings->coverAnsi, sizeof(settings->coverAnsi), "1") : c_strcpy(settings->coverAnsi, sizeof(settings->coverAnsi), "0"); if (settings->visualizerEnabled[0] == '\0') visualizerEnabled ? c_strcpy(settings->visualizerEnabled, sizeof(settings->visualizerEnabled), "1") : c_strcpy(settings->visualizerEnabled, sizeof(settings->visualizerEnabled), "0"); if (settings->useProfileColors[0] == '\0') useProfileColors ? c_strcpy(settings->useProfileColors, sizeof(settings->useProfileColors), "1") : c_strcpy(settings->useProfileColors, sizeof(settings->useProfileColors), "0"); if (settings->visualizerHeight[0] == '\0') { sprintf(settings->visualizerHeight, "%d", visualizerHeight); } if (settings->hideLogo[0] == '\0') hideLogo ? c_strcpy(settings->hideLogo, sizeof(settings->hideLogo), "1") : c_strcpy(settings->hideLogo, sizeof(settings->hideLogo), "0"); if (settings->hideHelp[0] == '\0') hideHelp ? c_strcpy(settings->hideHelp, sizeof(settings->hideHelp), "1") : c_strcpy(settings->hideHelp, sizeof(settings->hideHelp), "0"); sprintf(settings->cacheLibrary, "%d", cacheLibrary); int currentVolume = getCurrentVolume(); currentVolume = (currentVolume <= 0) ? 10 : currentVolume; sprintf(settings->lastVolume, "%d", currentVolume); // Null-terminate the character arrays settings->path[MAXPATHLEN - 1] = '\0'; settings->coverEnabled[1] = '\0'; settings->coverAnsi[1] = '\0'; settings->visualizerEnabled[1] = '\0'; settings->visualizerHeight[5] = '\0'; settings->lastVolume[5] = '\0'; settings->useProfileColors[1] = '\0'; settings->allowNotifications[1] = '\0'; settings->hideLogo[1] = '\0'; settings->hideHelp[1] = '\0'; settings->cacheLibrary[5] = '\0'; // Write the settings to the file fprintf(file, "# Make sure that kew is closed before editing this file in order for changes to take effect.\n\n"); fprintf(file, "path=%s\n", settings->path); // fprintf(file, "useThemeColors=%s\n", settings->useThemeColors); fprintf(file, "coverEnabled=%s\n", settings->coverEnabled); fprintf(file, "coverAnsi=%s\n", settings->coverAnsi); fprintf(file, "visualizerEnabled=%s\n", settings->visualizerEnabled); fprintf(file, "visualizerHeight=%s\n", settings->visualizerHeight); fprintf(file, "useProfileColors=%s\n", settings->useProfileColors); fprintf(file, "allowNotifications=%s\n", settings->allowNotifications); fprintf(file, "hideLogo=%s\n", settings->hideLogo); fprintf(file, "hideHelp=%s\n", settings->hideHelp); fprintf(file, "lastVolume=%s\n", settings->lastVolume); fprintf(file, "\n# Cache: Set to 1 to use cache of the music library directory tree for faster startup times.\n"); fprintf(file, "cacheLibrary=%s\n", settings->cacheLibrary); fprintf(file, "\n# Color values are 0=Black, 1=Red, 2=Green, 3=Yellow, 4=Blue, 5=Magenta, 6=Cyan, 7=White\n"); fprintf(file, "# These mostly affect the library view.\n\n"); fprintf(file, "# Logo color: \n"); fprintf(file, "color=%s\n", settings->color); fprintf(file, "# Header color in library view: \n"); fprintf(file, "artistColor=%s\n", settings->artistColor); fprintf(file, "# Now playing song text in library view: \n"); fprintf(file, "titleColor=%s\n", settings->titleColor); fprintf(file, "# Color of enqueued songs in library view: \n"); fprintf(file, "enqueuedColor=%s\n", settings->enqueuedColor); fprintf(file, "\n# Key Bindings:\n\n"); fprintf(file, "volumeUp=%s\n", settings->volumeUp); fprintf(file, "volumeUpAlt=%s\n", settings->volumeUpAlt); fprintf(file, "volumeDown=%s\n", settings->volumeDown); fprintf(file, "previousTrackAlt=%s\n", settings->previousTrackAlt); fprintf(file, "nextTrackAlt=%s\n", settings->nextTrackAlt); fprintf(file, "scrollUpAlt=%s\n", settings->scrollUpAlt); fprintf(file, "scrollDownAlt=%s\n", settings->scrollDownAlt); fprintf(file, "switchNumberedSong=%s\n", settings->switchNumberedSong); fprintf(file, "togglePause=%s\n", settings->togglePause); fprintf(file, "toggleColorsDerivedFrom=%s\n", settings->toggleColorsDerivedFrom); fprintf(file, "toggleVisualizer=%s\n", settings->toggleVisualizer); fprintf(file, "toggleAscii=%s\n", settings->toggleAscii); fprintf(file, "toggleRepeat=%s\n", settings->toggleRepeat); fprintf(file, "toggleShuffle=%s\n", settings->toggleShuffle); fprintf(file, "seekBackward=%s\n", settings->seekBackward); fprintf(file, "seekForward=%s\n", settings->seekForward); fprintf(file, "savePlaylist=%s\n", settings->savePlaylist); fprintf(file, "addToMainPlaylist=%s\n", settings->addToMainPlaylist); fprintf(file, "updateLibrary=%s\n", settings->updateLibrary); fprintf(file, "\n# The different main views, normally F2-F6: \n"); fprintf(file, "showPlaylistAlt=%s\n", settings->showPlaylistAlt); fprintf(file, "showLibraryAlt=%s\n", settings->showLibraryAlt); fprintf(file, "showTrackAlt=%s\n", settings->showTrackAlt); fprintf(file, "showSearchAlt=%s\n", settings->showSearchAlt); fprintf(file, "showKeysAlt=%s\n\n", settings->showKeysAlt); fprintf(file, "quit=%s\n\n", settings->quit); fprintf(file, "# For special keys use terminal codes: OS, for F4 for instance. This can depend on the terminal.\n"); fprintf(file, "# You can find out the codes for the keys by using tools like showkey.\n"); fprintf(file, "# For special keys, see the key value after the bracket \"[\" after typing \"showkey -a\" in the terminal and then pressing a key you want info about.\n"); fprintf(file, "\n\n"); fclose(file); free(filepath); free(configdir); } kew-2.8.2/src/settings.h000066400000000000000000000007151467402032100151130ustar00rootroot00000000000000#ifndef SETTINGS_H #define SETTINGS_H #include #include #include #include #include #include #include #include "file.h" #include "soundcommon.h" #include "player.h" #include "utils.h" #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif extern time_t lastTimeAppRan; extern AppSettings settings; void getConfig(AppSettings *settings); void setConfig(AppSettings *settings); #endif kew-2.8.2/src/songloader.c000066400000000000000000000233461467402032100154100ustar00rootroot00000000000000#include "songloader.h" /* songloader.c This file should contain only functions related to loading song data. */ #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif Cache *tempCache = NULL; void removeTagPrefix(char *value) { char *colon_pos = strchr(value, ':'); if (colon_pos) { // Remove the tag prefix by shifting the characters memmove(value, colon_pos + 1, strlen(colon_pos)); } } char *findLargestImageFile(const char *directoryPath, char *largestImageFile, off_t *largestFileSize) { DIR *directory = opendir(directoryPath); struct dirent *entry; struct stat fileStats; if (directory == NULL) { fprintf(stderr, "Failed to open directory: %s\n", directoryPath); return largestImageFile; } while ((entry = readdir(directory)) != NULL) { char filePath[MAXPATHLEN]; if (directoryPath[strlen(directoryPath) - 1] == '/') { snprintf(filePath, sizeof(filePath), "%s%s", directoryPath, entry->d_name); } else { snprintf(filePath, sizeof(filePath), "%s/%s", directoryPath, entry->d_name); } if (stat(filePath, &fileStats) == -1) { continue; } if (S_ISREG(fileStats.st_mode)) { // Check if the entry is an image file and has a larger size than the current largest image file char *extension = strrchr(entry->d_name, '.'); if (extension != NULL && (strcasecmp(extension, ".jpg") == 0 || strcasecmp(extension, ".jpeg") == 0 || strcasecmp(extension, ".png") == 0 || strcasecmp(extension, ".gif") == 0)) { if (fileStats.st_size > *largestFileSize) { *largestFileSize = fileStats.st_size; if (largestImageFile != NULL) { free(largestImageFile); } largestImageFile = strdup(filePath); } } } } closedir(directory); return largestImageFile; } void turnFilePathIntoTitle(const char *filePath, char *title) { char *lastSlash = strrchr(filePath, '/'); char *lastDot = strrchr(filePath, '.'); if (lastSlash != NULL && lastDot != NULL && lastDot > lastSlash) { size_t maxSize = sizeof(title) - 1; // Reserve space for null terminator snprintf(title, maxSize, "%s", lastSlash + 1); memcpy(title, lastSlash + 1, lastDot - lastSlash - 1); // Copy up to dst_size - 1 bytes title[lastDot - lastSlash - 1] = '\0'; trim(title); } } // Extracts metadata, returns -1 if no album cover found, -2 if no file found or if file has errors int extractTags(const char *input_file, TagSettings *tag_settings, double *duration, const char *coverFilePath) { AVFormatContext *fmt_ctx = NULL; AVDictionaryEntry *tag = NULL; int ret; if ((ret = avformat_open_input(&fmt_ctx, input_file, NULL, NULL)) < 0) { fprintf(stderr, "Could not open input file '%s'\n", input_file); return -2; } if ((ret = avformat_find_stream_info(fmt_ctx, NULL)) < 0) { fprintf(stderr, "Could not find stream information\n"); avformat_close_input(&fmt_ctx); return -2; } memset(tag_settings->title, 0, sizeof(tag_settings->title)); memset(tag_settings->artist, 0, sizeof(tag_settings->artist)); memset(tag_settings->album_artist, 0, sizeof(tag_settings->album_artist)); memset(tag_settings->album, 0, sizeof(tag_settings->album)); memset(tag_settings->date, 0, sizeof(tag_settings->date)); while ((tag = av_dict_get(fmt_ctx->metadata, "", tag, AV_DICT_IGNORE_SUFFIX))) { if (strcasecmp(tag->key, "title") == 0) { snprintf(tag_settings->title, sizeof(tag_settings->title), "%s", tag->value); } else if (strcasecmp(tag->key, "artist") == 0) { snprintf(tag_settings->artist, sizeof(tag_settings->artist), "%s", tag->value); } else if (strcasecmp(tag->key, "album_artist") == 0) { snprintf(tag_settings->album_artist, sizeof(tag_settings->album_artist), "%s", tag->value); } else if (strcasecmp(tag->key, "album") == 0) { snprintf(tag_settings->album, sizeof(tag_settings->album), "%s", tag->value); } else if (strcasecmp(tag->key, "date") == 0) { snprintf(tag_settings->date, sizeof(tag_settings->date), "%s", tag->value); } } if (strlen(tag_settings->title) <= 0) { char title[MAXPATHLEN]; turnFilePathIntoTitle(input_file, title); strncpy(tag_settings->title, title, sizeof(tag_settings->title) - 1); tag_settings->title[sizeof(tag_settings->title) - 1] = '\0'; } if (fmt_ctx->duration != AV_NOPTS_VALUE) { *duration = (double)(fmt_ctx->duration / AV_TIME_BASE); } else { *duration = 0.0; return -2; } int stream_index = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0); if (stream_index < 0) { fprintf(stderr, "Could not find a video stream in the input file\n"); avformat_close_input(&fmt_ctx); return -1; } AVStream *video_stream = fmt_ctx->streams[stream_index]; if (video_stream->disposition & AV_DISPOSITION_ATTACHED_PIC) { AVPacket *pkt = &video_stream->attached_pic; FILE *file = fopen(coverFilePath, "wb"); if (!file) { fprintf(stderr, "Could not open output file '%s'\n", coverFilePath); avformat_close_input(&fmt_ctx); return -1; } fwrite(pkt->data, 1, pkt->size, file); fclose(file); avformat_close_input(&fmt_ctx); return 0; } else { avformat_close_input(&fmt_ctx); return -1; } avformat_close_input(&fmt_ctx); return 0; } static guint track_counter = 0; // Generate a new track ID gchar *generateTrackId() { gchar *trackId = g_strdup_printf("/org/kew/tracklist/track%d", track_counter); track_counter++; return trackId; } void loadColor(SongData *songdata) { getCoverColor(songdata->cover, &(songdata->red), &(songdata->green), &(songdata->blue)); } void loadMetaData(SongData *songdata) { char path[MAXPATHLEN]; songdata->metadata = malloc(sizeof(TagSettings)); generateTempFilePath(songdata->coverArtPath, "cover", ".jpg"); int res = extractTags(songdata->filePath, songdata->metadata, &songdata->duration, songdata->coverArtPath); if (res == -2) { songdata->hasErrors = true; return; } else if (res == -1) { getDirectoryFromPath(songdata->filePath, path); char *tmp = NULL; off_t size = 0; tmp = findLargestImageFile(path, tmp, &size); if (tmp != NULL) c_strcpy(songdata->coverArtPath, sizeof(songdata->coverArtPath), tmp); else c_strcpy(songdata->coverArtPath, sizeof(songdata->coverArtPath), ""); } else { addToCache(tempCache, songdata->coverArtPath); } songdata->cover = getBitmap(songdata->coverArtPath); } SongData *loadSongData(char *filePath) { SongData *songdata = NULL; songdata = malloc(sizeof(SongData)); songdata->trackId = generateTrackId(); songdata->hasErrors = false; c_strcpy(songdata->filePath, sizeof(songdata->filePath), ""); c_strcpy(songdata->coverArtPath, sizeof(songdata->coverArtPath), ""); songdata->red = 150; songdata->green = 150; songdata->blue = 150; songdata->metadata = NULL; songdata->cover = NULL; songdata->duration = 0.0; c_strcpy(songdata->filePath, sizeof(songdata->filePath), filePath); loadMetaData(songdata); loadColor(songdata); return songdata; } void unloadSongData(SongData **songdata) { if (*songdata == NULL) return; SongData *data = *songdata; if (data->cover != NULL) { FreeImage_Unload(data->cover); data->cover = NULL; } if (existsInCache(tempCache, data->coverArtPath) && isInTempDir(data->coverArtPath)) { deleteFile(data->coverArtPath); } free(data->metadata); free(data->trackId); data->cover = NULL; data->metadata = NULL; data->trackId = NULL; free(*songdata); *songdata = NULL; } kew-2.8.2/src/songloader.h000066400000000000000000000022121467402032100154020ustar00rootroot00000000000000#include #include #include #include #include #include #include "cache.h" #include "chafafunc.h" #include "file.h" #include "sound.h" #include "soundcommon.h" #include "utils.h" #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif #ifndef KEYVALUEPAIR_STRUCT #define KEYVALUEPAIR_STRUCT typedef struct { char *key; char *value; } KeyValuePair; #endif #ifndef TAGSETTINGS_STRUCT #define TAGSETTINGS_STRUCT typedef struct { char title[256]; char artist[256]; char album_artist[256]; char album[256]; char date[256]; } TagSettings; #endif #ifndef SONGDATA_STRUCT #define SONGDATA_STRUCT typedef struct { gchar *trackId; char filePath[MAXPATHLEN]; char coverArtPath[MAXPATHLEN]; unsigned char red; unsigned char green; unsigned char blue; TagSettings *metadata; FIBITMAP *cover; double duration; bool hasErrors; } SongData; #endif extern Cache *tempCache; SongData *loadSongData(char *filePath); void unloadSongData(SongData **songdata); kew-2.8.2/src/sound.c000066400000000000000000000512221467402032100143750ustar00rootroot00000000000000#define MA_EXPERIMENTAL__DATA_LOOPING_AND_CHAINING #define MA_NO_ENGINE #define MINIAUDIO_IMPLEMENTATION #include #include "mpris.h" #include "sound.h" /* sound.c Functions related to miniaudio implementation */ ma_context context; bool isContextInitialized = false; UserData userData; int check_aac_codec_support() { const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_AAC); if (codec == NULL) { fprintf(stderr, "AAC codec not supported in this build of FFmpeg.\n"); return -1; } return 0; } ma_result initFirstDatasource(AudioData *pAudioData, UserData *pUserData) { char *filePath = NULL; filePath = (pAudioData->currentFileIndex == 0) ? pUserData->songdataA->filePath : pUserData->songdataB->filePath; pAudioData->pUserData = pUserData; pAudioData->currentPCMFrame = 0; pAudioData->restart = false; if (hasBuiltinDecoder(filePath)) { int result = prepareNextDecoder(filePath); if (result < 0) return -1; ma_decoder *first = getFirstDecoder(); pAudioData->format = first->outputFormat; pAudioData->channels = first->outputChannels; pAudioData->sampleRate = first->outputSampleRate; ma_data_source_get_length_in_pcm_frames(first, &pAudioData->totalFrames); } else if (endsWith(filePath, "opus")) { int result = prepareNextOpusDecoder(filePath); if (result < 0) return -1; ma_libopus *first = getFirstOpusDecoder(); ma_channel channelMap[MA_MAX_CHANNELS]; ma_libopus_ds_get_data_format(first, &pAudioData->format, &pAudioData->channels, &pAudioData->sampleRate, channelMap, MA_MAX_CHANNELS); ma_data_source_get_length_in_pcm_frames(first, &pAudioData->totalFrames); ma_data_source_base *base = (ma_data_source_base *)first; base->pCurrent = first; first->pReadSeekTellUserData = pAudioData; } else if (endsWith(filePath, "ogg")) { int result = prepareNextVorbisDecoder(filePath); if (result < 0) return -1; ma_libvorbis *first = getFirstVorbisDecoder(); ma_channel channelMap[MA_MAX_CHANNELS]; ma_libvorbis_ds_get_data_format(first, &pAudioData->format, &pAudioData->channels, &pAudioData->sampleRate, channelMap, MA_MAX_CHANNELS); ma_data_source_get_length_in_pcm_frames(first, &pAudioData->totalFrames); ma_data_source_base *base = (ma_data_source_base *)first; base->pCurrent = first; first->pReadSeekTellUserData = pAudioData; } else if (endsWith(filePath, "m4a") || endsWith(filePath, "aac")) { int result = prepareNextM4aDecoder(filePath); if (result < 0) return -1; m4a_decoder *first = getFirstM4aDecoder(); ma_channel channelMap[MA_MAX_CHANNELS]; m4a_decoder_ds_get_data_format(first, &pAudioData->format, &pAudioData->channels, &pAudioData->sampleRate, channelMap, MA_MAX_CHANNELS); ma_data_source_get_length_in_pcm_frames(first, &pAudioData->totalFrames); ma_data_source_base *base = (ma_data_source_base *)first; base->pCurrent = first; first->pReadSeekTellUserData = pAudioData; } else { return MA_ERROR; } return MA_SUCCESS; } int createDevice(UserData *userData, ma_device *device, ma_context *context, ma_data_source_vtable *vtable, ma_device_data_proc callback) { ma_result result; ma_data_source_uninit(&audioData); result = initFirstDatasource(&audioData, userData); if (result != MA_SUCCESS) return -1; audioData.base.vtable = vtable; ma_device_config deviceConfig = ma_device_config_init(ma_device_type_playback); deviceConfig.playback.format = audioData.format; deviceConfig.playback.channels = audioData.channels; deviceConfig.sampleRate = audioData.sampleRate; deviceConfig.dataCallback = callback; deviceConfig.pUserData = &audioData; result = ma_device_init(context, &deviceConfig, device); if (result != MA_SUCCESS) return -1; setVolume(getCurrentVolume()); result = ma_device_start(device); if (result != MA_SUCCESS) return -1; emitStringPropertyChanged("PlaybackStatus", "Playing"); return 0; } int builtin_createAudioDevice(UserData *userData, ma_device *device, ma_context *context, ma_data_source_vtable *vtable) { return createDevice(userData, device, context, vtable, builtin_on_audio_frames); } int vorbis_createAudioDevice(UserData *userData, ma_device *device, ma_context *context) { ma_result result; result = initFirstDatasource(&audioData, userData); if (result != MA_SUCCESS) { printf("\n\nFailed to initialize ogg vorbis file.\n"); return -1; } ma_libvorbis *vorbis = getFirstVorbisDecoder(); ma_device_config deviceConfig = ma_device_config_init(ma_device_type_playback); deviceConfig.playback.format = vorbis->format; deviceConfig.playback.channels = audioData.channels; deviceConfig.sampleRate = audioData.sampleRate; deviceConfig.dataCallback = vorbis_on_audio_frames; deviceConfig.pUserData = vorbis; result = ma_device_init(context, &deviceConfig, device); if (result != MA_SUCCESS) { printf("\n\nFailed to initialize miniaudio device.\n"); return -1; } setVolume(getCurrentVolume()); result = ma_device_start(device); if (result != MA_SUCCESS) { printf("\n\nFailed to start miniaudio device.\n"); return -1; } emitStringPropertyChanged("PlaybackStatus", "Playing"); return 0; } int m4a_createAudioDevice(UserData *userData, ma_device *device, ma_context *context) { ma_result result; result = initFirstDatasource(&audioData, userData); if (result != MA_SUCCESS) { printf("\n\nFailed to initialize m4a file.\n"); return -1; } m4a_decoder *decoder = getFirstM4aDecoder(); ma_device_config deviceConfig = ma_device_config_init(ma_device_type_playback); deviceConfig.playback.format = decoder->format; deviceConfig.playback.channels = audioData.channels; deviceConfig.sampleRate = audioData.sampleRate; deviceConfig.dataCallback = m4a_on_audio_frames; deviceConfig.pUserData = decoder; result = ma_device_init(context, &deviceConfig, device); if (result != MA_SUCCESS) { printf("\n\nFailed to initialize miniaudio device.\n"); return -1; } setVolume(getCurrentVolume()); result = ma_device_start(device); if (result != MA_SUCCESS) { printf("\n\nFailed to start miniaudio device.\n"); return -1; } emitStringPropertyChanged("PlaybackStatus", "Playing"); return 0; } int opus_createAudioDevice(UserData *userData, ma_device *device, ma_context *context) { ma_result result; result = initFirstDatasource(&audioData, userData); if (result != MA_SUCCESS) { printf("\n\nFailed to initialize opus file.\n"); return -1; } ma_libopus *opus = getFirstOpusDecoder(); ma_device_config deviceConfig = ma_device_config_init(ma_device_type_playback); deviceConfig.playback.format = opus->format; deviceConfig.playback.channels = audioData.channels; deviceConfig.sampleRate = audioData.sampleRate; deviceConfig.dataCallback = opus_on_audio_frames; deviceConfig.pUserData = opus; result = ma_device_init(context, &deviceConfig, device); if (result != MA_SUCCESS) { printf("\n\nFailed to initialize miniaudio device.\n"); return -1; } setVolume(getCurrentVolume()); result = ma_device_start(device); if (result != MA_SUCCESS) { printf("\n\nFailed to start miniaudio device.\n"); return -1; } emitStringPropertyChanged("PlaybackStatus", "Playing"); return 0; } bool validFilePath(char *filePath) { if (filePath == NULL || filePath[0] == '\0' || filePath[0] == '\r') return false; if (existsFile(filePath) < 0) return false; return true; } bool tryAgain = false; int switchAudioImplementation() { if (audioData.endOfListReached) { setEOFNotReached(); setCurrentImplementationType(NONE); return 0; } enum AudioImplementation currentImplementation = getCurrentImplementationType(); userData.currentSongData = (audioData.currentFileIndex == 0) ? userData.songdataA : userData.songdataB; char *filePath = NULL; if (userData.currentSongData == NULL) { setEOFNotReached(); return 0; } else { if (!validFilePath(userData.currentSongData->filePath)) { if (!tryAgain) { setCurrentFileIndex(&audioData, 1 - audioData.currentFileIndex); tryAgain = true; switchAudioImplementation(); return 0; } else { setEOFReached(); return -1; } } filePath = strdup(userData.currentSongData->filePath); } tryAgain = false; if (hasBuiltinDecoder(filePath)) { ma_uint32 sampleRate = 0; ma_uint32 channels = 0; ma_format format = ma_format_unknown; ma_decoder *decoder = getCurrentBuiltinDecoder(); getFileInfo(filePath, &sampleRate, &channels, &format); bool sameFormat = (decoder != NULL && (sampleRate == decoder->outputSampleRate && channels == decoder->outputChannels && format == decoder->outputFormat)); if (isRepeatEnabled() || !(sameFormat && currentImplementation == BUILTIN)) { setImplSwitchReached(); pthread_mutex_lock(&dataSourceMutex); setCurrentImplementationType(BUILTIN); cleanupPlaybackDevice(); resetDecoders(); resetVorbisDecoders(); resetM4aDecoders(); resetOpusDecoders(); resetAudioBuffer(); int result = builtin_createAudioDevice(&userData, getDevice(), &context, &builtin_file_data_source_vtable); if (result < 0) { setCurrentImplementationType(NONE); setImplSwitchNotReached(); setEOFReached(); free(filePath); pthread_mutex_unlock(&dataSourceMutex); return -1; } pthread_mutex_unlock(&dataSourceMutex); setImplSwitchNotReached(); } } else if (endsWith(filePath, "opus")) { ma_uint32 sampleRate; ma_uint32 channels; ma_format format; ma_channel channelMap[MA_MAX_CHANNELS]; ma_uint32 nSampleRate; ma_uint32 nChannels; ma_format nFormat; ma_channel nChannelMap[MA_MAX_CHANNELS]; ma_libopus *decoder = getCurrentOpusDecoder(); getOpusFileInfo(filePath, &format, &channels, &sampleRate, channelMap); if (decoder != NULL) ma_libopus_ds_get_data_format(decoder, &nFormat, &nChannels, &nSampleRate, nChannelMap, MA_MAX_CHANNELS); bool sameFormat = (decoder != NULL && (format == decoder->format && channels == nChannels && sampleRate == nSampleRate)); if (isRepeatEnabled() || !(sameFormat && currentImplementation == OPUS)) { setImplSwitchReached(); pthread_mutex_lock(&dataSourceMutex); setCurrentImplementationType(OPUS); cleanupPlaybackDevice(); resetDecoders(); resetVorbisDecoders(); resetM4aDecoders(); resetOpusDecoders(); resetAudioBuffer(); int result = opus_createAudioDevice(&userData, getDevice(), &context); if (result < 0) { setCurrentImplementationType(NONE); setImplSwitchNotReached(); setEOFReached(); free(filePath); pthread_mutex_unlock(&dataSourceMutex); return -1; } pthread_mutex_unlock(&dataSourceMutex); setImplSwitchNotReached(); } } else if (endsWith(filePath, "ogg")) { ma_uint32 sampleRate; ma_uint32 channels; ma_format format; ma_channel channelMap[MA_MAX_CHANNELS]; ma_uint32 nSampleRate; ma_uint32 nChannels; ma_format nFormat; ma_channel nChannelMap[MA_MAX_CHANNELS]; ma_libvorbis *decoder = getCurrentVorbisDecoder(); getVorbisFileInfo(filePath, &format, &channels, &sampleRate, channelMap); if (decoder != NULL) ma_libvorbis_ds_get_data_format(decoder, &nFormat, &nChannels, &nSampleRate, nChannelMap, MA_MAX_CHANNELS); bool sameFormat = (decoder != NULL && (format == decoder->format && channels == nChannels && sampleRate == nSampleRate)); if (isRepeatEnabled() || !(sameFormat && currentImplementation == VORBIS)) { setImplSwitchReached(); pthread_mutex_lock(&dataSourceMutex); setCurrentImplementationType(VORBIS); cleanupPlaybackDevice(); resetDecoders(); resetVorbisDecoders(); resetM4aDecoders(); resetOpusDecoders(); resetAudioBuffer(); int result = vorbis_createAudioDevice(&userData, getDevice(), &context); if (result < 0) { setCurrentImplementationType(NONE); setImplSwitchNotReached(); setEOFReached(); free(filePath); pthread_mutex_unlock(&dataSourceMutex); return -1; } pthread_mutex_unlock(&dataSourceMutex); setImplSwitchNotReached(); } } else if (endsWith(filePath, "m4a") || endsWith(filePath, "aac")) { if (check_aac_codec_support() < 0) { free(filePath); printf("\n\nUnable to find AAC codec. If you have the free version of FFmpeg, there might be no AAC/M4A file support.\n"); exit(0); } ma_uint32 sampleRate; ma_uint32 channels; ma_format format; ma_channel channelMap[MA_MAX_CHANNELS]; ma_uint32 nSampleRate; ma_uint32 nChannels; ma_format nFormat; ma_channel nChannelMap[MA_MAX_CHANNELS]; m4a_decoder *decoder = getCurrentM4aDecoder(); getM4aFileInfo(filePath, &format, &channels, &sampleRate, channelMap); if (decoder != NULL) m4a_decoder_ds_get_data_format(decoder, &nFormat, &nChannels, &nSampleRate, nChannelMap, MA_MAX_CHANNELS); bool sameFormat = (decoder != NULL && (format == decoder->format && channels == nChannels && sampleRate == nSampleRate)); if (isRepeatEnabled() || !(sameFormat && currentImplementation == M4A)) { setImplSwitchReached(); pthread_mutex_lock(&dataSourceMutex); setCurrentImplementationType(M4A); cleanupPlaybackDevice(); resetDecoders(); resetVorbisDecoders(); resetM4aDecoders(); resetOpusDecoders(); resetAudioBuffer(); int result = m4a_createAudioDevice(&userData, getDevice(), &context); if (result < 0) { setCurrentImplementationType(NONE); setImplSwitchNotReached(); setEOFReached(); free(filePath); pthread_mutex_unlock(&dataSourceMutex); return -1; } pthread_mutex_unlock(&dataSourceMutex); setImplSwitchNotReached(); } } else { free(filePath); return -1; } free(filePath); setEOFNotReached(); return 0; } void cleanupAudioContext() { ma_context_uninit(&context); isContextInitialized = false; } int createAudioDevice(UserData *userData) { if (isContextInitialized) { ma_context_uninit(&context); isContextInitialized = false; } ma_context_init(NULL, 0, NULL, &context); isContextInitialized = true; if (switchAudioImplementation() >= 0) { SongData *currentSongData = userData->currentSongData; if (currentSongData != NULL && currentSongData->hasErrors == 0 && currentSongData->metadata && strlen(currentSongData->metadata->title) > 0) { #ifdef USE_LIBNOTIFY displaySongNotification(currentSongData->metadata->artist, currentSongData->metadata->title, currentSongData->coverArtPath); #endif gint64 length = getLengthInMicroSec(currentSongData->duration); // update mpris emitMetadataChanged( currentSongData->metadata->title, currentSongData->metadata->artist, currentSongData->metadata->album, currentSongData->coverArtPath, currentSongData->trackId != NULL ? currentSongData->trackId : "", currentSong, length); } } else { return -1; } return 0; } kew-2.8.2/src/sound.h000066400000000000000000000027101467402032100144000ustar00rootroot00000000000000#ifndef SOUND_H #define SOUND_H #include #include #include #include #include #include #include #include #include #include #include #include "file.h" #include "songloader.h" #include "soundbuiltin.h" #include "soundcommon.h" #ifndef USERDATA_STRUCT #define USERDATA_STRUCT typedef struct { SongData *songdataA; SongData *songdataB; bool songdataADeleted; bool songdataBDeleted; SongData *currentSongData; ma_uint32 currentPCMFrame; } UserData; #endif #ifndef AUDIODATA_STRUCT #define AUDIODATA_STRUCT typedef struct { ma_data_source_base base; UserData *pUserData; ma_format format; ma_uint32 channels; ma_uint32 sampleRate; ma_uint32 currentPCMFrame; bool switchFiles; int currentFileIndex; ma_uint64 totalFrames; bool endOfListReached; bool restart; } AudioData; #endif extern AudioData audioData; extern UserData userData; extern bool isContextInitialized; int prepareNextDecoder(char *filepath); int prepareNextOpusDecoder(char *filepath); int prepareNextVorbisDecoder(char *filepath); int prepareNextM4aDecoder(char *filepath); void setDecoders(bool usingA, char *filePath); int createAudioDevice(UserData *userData); int switchAudioImplementation(); void cleanupAudioContext(); #endif kew-2.8.2/src/soundbuiltin.c000066400000000000000000000155031467402032100157660ustar00rootroot00000000000000#include "soundbuiltin.h" /* soundbuiltin.c Functions related to miniaudio implementation for miniaudio built-in decoders (flac, wav and mp3) */ static ma_result builtin_file_data_source_read(ma_data_source *pDataSource, void *pFramesOut, ma_uint64 frameCount, ma_uint64 *pFramesRead) { // Dummy implementation (void)pDataSource; (void)pFramesOut; (void)frameCount; (void)pFramesRead; return MA_SUCCESS; } static ma_result builtin_file_data_source_seek(ma_data_source *pDataSource, ma_uint64 frameIndex) { AudioData *audioData = (AudioData *)pDataSource; if (getCurrentBuiltinDecoder() == NULL) { return MA_INVALID_ARGS; } ma_result result = ma_decoder_seek_to_pcm_frame(getCurrentBuiltinDecoder(), frameIndex); if (result == MA_SUCCESS) { audioData->currentPCMFrame = (ma_uint32)frameIndex; return MA_SUCCESS; } else { return result; } } static ma_result builtin_file_data_source_get_data_format(ma_data_source *pDataSource, ma_format *pFormat, ma_uint32 *pChannels, ma_uint32 *pSampleRate, ma_channel *pChannelMap, size_t channelMapCap) { (void)pChannelMap; (void)channelMapCap; AudioData *audioData = (AudioData *)pDataSource; *pFormat = audioData->format; *pChannels = audioData->channels; *pSampleRate = audioData->sampleRate; return MA_SUCCESS; } static ma_result builtin_file_data_source_get_cursor(ma_data_source *pDataSource, ma_uint64 *pCursor) { AudioData *audioData = (AudioData *)pDataSource; *pCursor = audioData->currentPCMFrame; return MA_SUCCESS; } static ma_result builtin_file_data_source_get_length(ma_data_source *pDataSource, ma_uint64 *pLength) { (void)pDataSource; ma_uint64 totalFrames = 0; if (getCurrentBuiltinDecoder() == NULL) { return MA_INVALID_ARGS; } ma_result result = ma_decoder_get_length_in_pcm_frames(getCurrentBuiltinDecoder(), &totalFrames); if (result != MA_SUCCESS) { return result; } *pLength = totalFrames; return MA_SUCCESS; } static ma_result builtin_file_data_source_set_looping(ma_data_source *pDataSource, ma_bool32 isLooping) { // Dummy implementation (void)pDataSource; (void)isLooping; return MA_SUCCESS; } ma_data_source_vtable builtin_file_data_source_vtable = { builtin_file_data_source_read, builtin_file_data_source_seek, builtin_file_data_source_get_data_format, builtin_file_data_source_get_cursor, builtin_file_data_source_get_length, builtin_file_data_source_set_looping, 0 // flags }; void builtin_read_pcm_frames(ma_data_source *pDataSource, void *pFramesOut, ma_uint64 frameCount, ma_uint64 *pFramesRead) { AudioData *audioData = (AudioData *)pDataSource; ma_uint64 framesRead = 0; while (framesRead < frameCount) { ma_uint64 remainingFrames = frameCount - framesRead; if (pthread_mutex_trylock(&dataSourceMutex) != 0) { return; } if (isImplSwitchReached() || audioData == NULL) { pthread_mutex_unlock(&dataSourceMutex); return; } if (audioData->switchFiles) { executeSwitch(audioData); pthread_mutex_unlock(&dataSourceMutex); break; } ma_decoder *decoder = getCurrentBuiltinDecoder(); if ((getCurrentImplementationType() != BUILTIN && !isSkipToNext())) { pthread_mutex_unlock(&dataSourceMutex); return; } if (audioData->totalFrames == 0) ma_data_source_get_length_in_pcm_frames(decoder, &audioData->totalFrames); if (isSeekRequested()) { ma_uint64 totalFrames = audioData->totalFrames; ma_uint64 seekPercent = getSeekPercentage(); if (seekPercent >= 100.0) seekPercent = 100.0; ma_uint64 targetFrame = (totalFrames * seekPercent) / 100; ma_result seekResult = ma_decoder_seek_to_pcm_frame(decoder, targetFrame); if (seekResult != MA_SUCCESS) { setSeekRequested(false); pthread_mutex_unlock(&dataSourceMutex); return; } setSeekRequested(false); } ma_uint64 framesToRead = 0; ma_decoder *firstDecoder = getFirstDecoder(); ma_uint64 cursor = 0; ma_result result; if (firstDecoder == NULL) { pthread_mutex_unlock(&dataSourceMutex); return; } result = ma_data_source_read_pcm_frames(firstDecoder, (ma_int32 *)pFramesOut + framesRead * audioData->channels, remainingFrames, &framesToRead); ma_data_source_get_cursor_in_pcm_frames(decoder, &cursor); if (((audioData->totalFrames != 0 && cursor != 0 && cursor >= audioData->totalFrames) || framesToRead == 0 || isSkipToNext() || result != MA_SUCCESS) && !isEOFReached()) { activateSwitch(audioData); pthread_mutex_unlock(&dataSourceMutex); continue; } framesRead += framesToRead; setBufferSize(framesToRead); pthread_mutex_unlock(&dataSourceMutex); } ma_int32 *audioBuffer = getAudioBuffer(); if (audioBuffer == NULL) { audioBuffer = malloc(sizeof(ma_int32) * MAX_BUFFER_SIZE); if (audioBuffer == NULL) { return; } } memcpy(audioBuffer, pFramesOut, sizeof(ma_int32) * framesRead); setAudioBuffer(audioBuffer); if (pFramesRead != NULL) { *pFramesRead = framesRead; } } void builtin_on_audio_frames(ma_device *pDevice, void *pFramesOut, const void *pFramesIn, ma_uint32 frameCount) { AudioData *pDataSource = (AudioData *)pDevice->pUserData; ma_uint64 framesRead = 0; builtin_read_pcm_frames(&pDataSource->base, pFramesOut, frameCount, &framesRead); (void)pFramesIn; } kew-2.8.2/src/soundbuiltin.h000066400000000000000000000006711467402032100157730ustar00rootroot00000000000000#ifndef SOUNDBUILTIN_H #define SOUNDBUILTIN_H #include #include #include #include "soundcommon.h" extern ma_data_source_vtable builtin_file_data_source_vtable; void builtin_read_pcm_frames(ma_data_source *pDataSource, void *pFramesOut, ma_uint64 frameCount, ma_uint64 *pFramesRead); void builtin_on_audio_frames(ma_device *pDevice, void *pFramesOut, const void *pFramesIn, ma_uint32 frameCount); #endif kew-2.8.2/src/soundcommon.c000066400000000000000000001432361467402032100156150ustar00rootroot00000000000000#include "soundcommon.h" /* soundcommon.c Related to common functions for decoders / miniaudio implementations. */ #define MAX_DECODERS 2 bool allowNotifications = true; bool repeatEnabled = false; bool shuffleEnabled = false; bool skipToNext = false; bool seekRequested = false; bool paused = false; bool stopped = true; bool hasSilentlySwitched; float seekPercent = 0.0; double seekElapsed; _Atomic bool EOFReached = false; _Atomic bool switchReached = false; _Atomic bool readingFrames = false; pthread_mutex_t dataSourceMutex = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t switchMutex = PTHREAD_MUTEX_INITIALIZER; ma_device device = {0}; ma_int32 *audioBuffer = NULL; AudioData audioData; int bufSize; ma_event switchAudioImpl; enum AudioImplementation currentImplementation = NONE; bool doQuit = false; AppState appState; volatile bool refresh = true; double duration; double elapsedSeconds = 0.0; int soundVolume = 100; ma_decoder *firstDecoder; ma_decoder *currentDecoder; ma_decoder *decoders[MAX_DECODERS]; ma_libopus *opusDecoders[MAX_DECODERS]; ma_libopus *firstOpusDecoder; ma_libvorbis *vorbisDecoders[MAX_DECODERS]; ma_libvorbis *firstVorbisDecoder; m4a_decoder *m4aDecoders[MAX_DECODERS]; m4a_decoder *firstM4aDecoder; int decoderIndex = -1; int m4aDecoderIndex = -1; int opusDecoderIndex = -1; int vorbisDecoderIndex = -1; #ifdef USE_LIBNOTIFY NotifyNotification *previous_notification; #endif void logTime(const char *message) { (void)message; // struct timespec ts; // clock_gettime(CLOCK_REALTIME, &ts); // printf("[%ld.%09ld] %s\n", ts.tv_sec, ts.tv_nsec, message); } enum AudioImplementation getCurrentImplementationType() { return currentImplementation; } void setCurrentImplementationType(enum AudioImplementation value) { currentImplementation = value; } ma_decoder *getFirstDecoder() { return firstDecoder; } ma_decoder *getCurrentBuiltinDecoder() { if (decoderIndex == -1) return getFirstDecoder(); else return decoders[decoderIndex]; } void switchDecoder() { if (decoderIndex == -1) decoderIndex = 0; else decoderIndex = 1 - decoderIndex; } void resetDecoders() { decoderIndex = -1; if (firstDecoder != NULL && firstDecoder->outputFormat != ma_format_unknown) { ma_decoder_uninit(firstDecoder); free(firstDecoder); firstDecoder = NULL; } if (decoders[0] != NULL && decoders[0]->outputFormat != ma_format_unknown) { ma_decoder_uninit(decoders[0]); free(decoders[0]); decoders[0] = NULL; } if (decoders[1] != NULL && decoders[1]->outputFormat != ma_format_unknown) { ma_decoder_uninit(decoders[1]); free(decoders[1]); decoders[1] = NULL; } } void uninitPreviousDecoder() { if (decoderIndex == -1) { return; } ma_decoder *toUninit = decoders[1 - decoderIndex]; if (toUninit != NULL) { ma_decoder_uninit(toUninit); free(toUninit); decoders[1 - decoderIndex] = NULL; } } void uninitPreviousVorbisDecoder() { if (vorbisDecoderIndex == -1) { return; } ma_libvorbis *toUninit = vorbisDecoders[1 - vorbisDecoderIndex]; if (toUninit != NULL) { ma_libvorbis_uninit(toUninit, NULL); free(toUninit); vorbisDecoders[1 - vorbisDecoderIndex] = NULL; } } void uninitPreviousM4aDecoder() { if (m4aDecoderIndex == -1) // either start of the program or resetM4aDecoders has been called { return; } m4a_decoder *toUninit = m4aDecoders[1 - m4aDecoderIndex]; if (toUninit != NULL) { m4a_decoder_uninit(toUninit, NULL); free(toUninit); m4aDecoders[1 - m4aDecoderIndex] = NULL; } } void uninitPreviousOpusDecoder() { if (opusDecoderIndex == -1) { return; } ma_libopus *toUninit = opusDecoders[1 - opusDecoderIndex]; if (toUninit != NULL) { ma_libopus_uninit(toUninit, NULL); free(toUninit); opusDecoders[1 - opusDecoderIndex] = NULL; } } ma_libvorbis *getFirstVorbisDecoder() { return firstVorbisDecoder; } m4a_decoder *getFirstM4aDecoder() { return firstM4aDecoder; } ma_libopus *getFirstOpusDecoder() { return firstOpusDecoder; } ma_libvorbis *getCurrentVorbisDecoder() { if (vorbisDecoderIndex == -1) return getFirstVorbisDecoder(); else return vorbisDecoders[vorbisDecoderIndex]; } m4a_decoder *getCurrentM4aDecoder() { if (m4aDecoderIndex == -1) return getFirstM4aDecoder(); else return m4aDecoders[m4aDecoderIndex]; } ma_libopus *getCurrentOpusDecoder() { if (opusDecoderIndex == -1) return getFirstOpusDecoder(); else return opusDecoders[opusDecoderIndex]; } ma_format getCurrentFormat() { ma_format format = ma_format_unknown; if (getCurrentImplementationType() == BUILTIN) { ma_decoder *decoder = getCurrentBuiltinDecoder(); if (decoder != NULL) format = decoder->outputFormat; } else if (getCurrentImplementationType() == OPUS) { ma_libopus *decoder = getCurrentOpusDecoder(); if (decoder != NULL) format = decoder->format; } else if (getCurrentImplementationType() == VORBIS) { ma_libvorbis *decoder = getCurrentVorbisDecoder(); if (decoder != NULL) format = decoder->format; } else if (getCurrentImplementationType() == M4A) { m4a_decoder *decoder = getCurrentM4aDecoder(); if (decoder != NULL) format = decoder->format; } return format; } void switchVorbisDecoder() { if (vorbisDecoderIndex == -1) vorbisDecoderIndex = 0; else vorbisDecoderIndex = 1 - vorbisDecoderIndex; } void switchM4aDecoder() { if (m4aDecoderIndex == -1) m4aDecoderIndex = 0; else m4aDecoderIndex = 1 - m4aDecoderIndex; } void switchOpusDecoder() { if (opusDecoderIndex == -1) opusDecoderIndex = 0; else opusDecoderIndex = 1 - opusDecoderIndex; } void setNextVorbisDecoder(ma_libvorbis *decoder) { if (vorbisDecoderIndex == -1 && firstVorbisDecoder == NULL) { firstVorbisDecoder = decoder; } else if (vorbisDecoderIndex == -1) { if (vorbisDecoders[0] != NULL) { ma_libvorbis_uninit(vorbisDecoders[0], NULL); free(vorbisDecoders[0]); vorbisDecoders[0] = NULL; } vorbisDecoders[0] = decoder; } else { int nextIndex = 1 - vorbisDecoderIndex; if (vorbisDecoders[nextIndex] != NULL) { ma_libvorbis_uninit(vorbisDecoders[nextIndex], NULL); free(vorbisDecoders[nextIndex]); vorbisDecoders[nextIndex] = NULL; } vorbisDecoders[nextIndex] = decoder; } } void setNextM4aDecoder(m4a_decoder *decoder) { if (m4aDecoderIndex == -1 && firstM4aDecoder == NULL) { firstM4aDecoder = decoder; } else if (m4aDecoderIndex == -1) // array hasn't been used yet { if (m4aDecoders[0] != NULL) { m4a_decoder_uninit(m4aDecoders[0], NULL); free(m4aDecoders[0]); m4aDecoders[0] = NULL; } m4aDecoders[0] = decoder; } else { int nextIndex = 1 - m4aDecoderIndex; if (m4aDecoders[nextIndex] != NULL) { m4a_decoder_uninit(m4aDecoders[nextIndex], NULL); free(m4aDecoders[nextIndex]); m4aDecoders[nextIndex] = NULL; } m4aDecoders[nextIndex] = decoder; } } void setNextOpusDecoder(ma_libopus *decoder) { if (opusDecoderIndex == -1 && firstOpusDecoder == NULL) { firstOpusDecoder = decoder; } else if (opusDecoderIndex == -1) { if (opusDecoders[0] != NULL) { ma_libopus_uninit(opusDecoders[0], NULL); free(opusDecoders[0]); opusDecoders[0] = NULL; } opusDecoders[0] = decoder; } else { int nextIndex = 1 - opusDecoderIndex; if (opusDecoders[nextIndex] != NULL) { ma_libopus_uninit(opusDecoders[nextIndex], NULL); free(opusDecoders[nextIndex]); opusDecoders[nextIndex] = NULL; } opusDecoders[nextIndex] = decoder; } } void resetVorbisDecoders() { vorbisDecoderIndex = -1; if (firstVorbisDecoder != NULL && firstVorbisDecoder->format != ma_format_unknown) { ma_libvorbis_uninit(firstVorbisDecoder, NULL); free(firstVorbisDecoder); firstVorbisDecoder = NULL; } if (vorbisDecoders[0] != NULL && vorbisDecoders[0]->format != ma_format_unknown) { ma_libvorbis_uninit(vorbisDecoders[0], NULL); free(vorbisDecoders[0]); vorbisDecoders[0] = NULL; } if (vorbisDecoders[1] != NULL && vorbisDecoders[1]->format != ma_format_unknown) { ma_libvorbis_uninit(vorbisDecoders[1], NULL); free(vorbisDecoders[1]); vorbisDecoders[1] = NULL; } } void resetM4aDecoders() { m4aDecoderIndex = -1; if (firstM4aDecoder != NULL && firstM4aDecoder->format != ma_format_unknown) { m4a_decoder_uninit(firstM4aDecoder, NULL); free(firstM4aDecoder); firstM4aDecoder = NULL; } if (m4aDecoders[0] != NULL && m4aDecoders[0]->format != ma_format_unknown) { m4a_decoder_uninit(m4aDecoders[0], NULL); free(m4aDecoders[0]); m4aDecoders[0] = NULL; } if (m4aDecoders[1] != NULL && m4aDecoders[1]->format != ma_format_unknown) { m4a_decoder_uninit(m4aDecoders[1], NULL); free(m4aDecoders[1]); m4aDecoders[1] = NULL; } } void resetOpusDecoders() { opusDecoderIndex = -1; if (firstOpusDecoder != NULL && firstOpusDecoder->format != ma_format_unknown) { ma_libopus_uninit(firstOpusDecoder, NULL); free(firstOpusDecoder); firstOpusDecoder = NULL; } if (opusDecoders[0] != NULL && opusDecoders[0]->format != ma_format_unknown) { ma_libopus_uninit(opusDecoders[0], NULL); ma_free(opusDecoders[0], NULL); opusDecoders[0] = NULL; } if (opusDecoders[1] != NULL && opusDecoders[1]->format != ma_format_unknown) { ma_libopus_uninit(opusDecoders[1], NULL); ma_free(opusDecoders[1], NULL); opusDecoders[1] = NULL; } } void getFileInfo(const char *filename, ma_uint32 *sampleRate, ma_uint32 *channels, ma_format *format) { ma_decoder tmp; if (ma_decoder_init_file(filename, NULL, &tmp) == MA_SUCCESS) { *sampleRate = tmp.outputSampleRate; *channels = tmp.outputChannels; *format = tmp.outputFormat; ma_decoder_uninit(&tmp); } else { // Handle file open error. } } void getVorbisFileInfo(const char *filename, ma_format *format, ma_uint32 *channels, ma_uint32 *sampleRate, ma_channel *channelMap) { ma_libvorbis decoder; if (ma_libvorbis_init_file(filename, NULL, NULL, &decoder) == MA_SUCCESS) { *format = decoder.format; ma_libvorbis_get_data_format(&decoder, format, channels, sampleRate, channelMap, MA_MAX_CHANNELS); ma_libvorbis_uninit(&decoder, NULL); } } void getM4aFileInfo(const char *filename, ma_format *format, ma_uint32 *channels, ma_uint32 *sampleRate, ma_channel *channelMap) { m4a_decoder decoder; if (m4a_decoder_init_file(filename, NULL, NULL, &decoder) == MA_SUCCESS) { *format = decoder.format; m4a_decoder_get_data_format(&decoder, format, channels, sampleRate, channelMap, MA_MAX_CHANNELS); m4a_decoder_uninit(&decoder, NULL); } } void getOpusFileInfo(const char *filename, ma_format *format, ma_uint32 *channels, ma_uint32 *sampleRate, ma_channel *channelMap) { ma_libopus decoder; if (ma_libopus_init_file(filename, NULL, NULL, &decoder) == MA_SUCCESS) { *format = decoder.format; ma_libopus_get_data_format(&decoder, format, channels, sampleRate, channelMap, MA_MAX_CHANNELS); ma_libopus_uninit(&decoder, NULL); } } void setNextDecoder(ma_decoder *decoder) { if (decoderIndex == -1 && firstDecoder == NULL) { firstDecoder = decoder; } else if (decoderIndex == -1) { if (decoders[0] != NULL) { ma_decoder_uninit(decoders[0]); free(decoders[0]); decoders[0] = NULL; } decoders[0] = decoder; } else { int nextIndex = 1 - decoderIndex; if (decoders[nextIndex] != NULL) { ma_decoder_uninit(decoders[nextIndex]); free(decoders[nextIndex]); decoders[nextIndex] = NULL; } decoders[nextIndex] = decoder; } } MA_API ma_result m4a_read_pcm_frames_wrapper(void *pDecoder, void *pFramesOut, size_t frameCount, size_t *pFramesRead) { ma_decoder *dec = (ma_decoder *)pDecoder; return m4a_decoder_read_pcm_frames((m4a_decoder *)dec->pUserData, pFramesOut, frameCount, (ma_uint64 *)pFramesRead); } MA_API ma_result m4a_seek_to_pcm_frame_wrapper(void *pDecoder, long long int frameIndex, ma_seek_origin origin) { (void)origin; ma_decoder *dec = (ma_decoder *)pDecoder; return m4a_decoder_seek_to_pcm_frame((m4a_decoder *)dec->pUserData, frameIndex); } MA_API ma_result m4a_get_cursor_in_pcm_frames_wrapper(void *pDecoder, long long int *pCursor) { ma_decoder *dec = (ma_decoder *)pDecoder; return m4a_decoder_get_cursor_in_pcm_frames((m4a_decoder *)dec->pUserData, (ma_uint64 *)pCursor); } MA_API ma_result ma_libopus_read_pcm_frames_wrapper(void *pDecoder, void *pFramesOut, size_t frameCount, size_t *pFramesRead) { ma_decoder *dec = (ma_decoder *)pDecoder; return ma_libopus_read_pcm_frames((ma_libopus *)dec->pUserData, pFramesOut, frameCount, (ma_uint64 *)pFramesRead); } MA_API ma_result ma_libopus_seek_to_pcm_frame_wrapper(void *pDecoder, long long int frameIndex, ma_seek_origin origin) { (void)origin; ma_decoder *dec = (ma_decoder *)pDecoder; return ma_libopus_seek_to_pcm_frame((ma_libopus *)dec->pUserData, frameIndex); } MA_API ma_result ma_libopus_get_cursor_in_pcm_frames_wrapper(void *pDecoder, long long int *pCursor) { ma_decoder *dec = (ma_decoder *)pDecoder; return ma_libopus_get_cursor_in_pcm_frames((ma_libopus *)dec->pUserData, (ma_uint64 *)pCursor); } MA_API ma_result ma_libvorbis_read_pcm_frames_wrapper(void *pDecoder, void *pFramesOut, size_t frameCount, size_t *pFramesRead) { ma_decoder *dec = (ma_decoder *)pDecoder; return ma_libvorbis_read_pcm_frames((ma_libvorbis *)dec->pUserData, pFramesOut, frameCount, (ma_uint64 *)pFramesRead); } MA_API ma_result ma_libvorbis_seek_to_pcm_frame_wrapper(void *pDecoder, long long int frameIndex, ma_seek_origin origin) { (void)origin; ma_decoder *dec = (ma_decoder *)pDecoder; return ma_libvorbis_seek_to_pcm_frame((ma_libvorbis *)dec->pUserData, frameIndex); } MA_API ma_result ma_libvorbis_get_cursor_in_pcm_frames_wrapper(void *pDecoder, long long int *pCursor) { ma_decoder *dec = (ma_decoder *)pDecoder; return ma_libvorbis_get_cursor_in_pcm_frames((ma_libvorbis *)dec->pUserData, (ma_uint64 *)pCursor); } int prepareNextVorbisDecoder(char *filepath) { ma_libvorbis *currentDecoder; if (vorbisDecoderIndex == -1) { currentDecoder = getFirstVorbisDecoder(); } else { currentDecoder = vorbisDecoders[vorbisDecoderIndex]; } ma_uint32 sampleRate; ma_uint32 channels; ma_format format; ma_channel channelMap[MA_MAX_CHANNELS]; ma_libvorbis_get_data_format(currentDecoder, &format, &channels, &sampleRate, channelMap, MA_MAX_CHANNELS); uninitPreviousVorbisDecoder(); ma_libvorbis *decoder = (ma_libvorbis *)malloc(sizeof(ma_libvorbis)); ma_result result = ma_libvorbis_init_file(filepath, NULL, NULL, decoder); if (result != MA_SUCCESS) return -1; ma_format nformat; ma_uint32 nchannels; ma_uint32 nsampleRate; ma_channel nchannelMap[MA_MAX_CHANNELS]; ma_libvorbis_get_data_format(decoder, &nformat, &nchannels, &nsampleRate, nchannelMap, MA_MAX_CHANNELS); bool sameFormat = (currentDecoder == NULL || (format == nformat && channels == nchannels && sampleRate == nsampleRate)); if (!sameFormat) { ma_libvorbis_uninit(decoder, NULL); free(decoder); return -1; } ma_libvorbis *first = getFirstVorbisDecoder(); if (first != NULL) { decoder->pReadSeekTellUserData = (AudioData *)first->pReadSeekTellUserData; } decoder->format = nformat; decoder->onRead = ma_libvorbis_read_pcm_frames_wrapper; decoder->onSeek = ma_libvorbis_seek_to_pcm_frame_wrapper; decoder->onTell = ma_libvorbis_get_cursor_in_pcm_frames_wrapper; setNextVorbisDecoder(decoder); if (currentDecoder != NULL) ma_data_source_set_next(currentDecoder, decoder); return 0; } int prepareNextDecoder(char *filepath) { ma_decoder *currentDecoder; if (decoderIndex == -1) { currentDecoder = getFirstDecoder(); } else { currentDecoder = decoders[decoderIndex]; } ma_uint32 sampleRate; ma_uint32 channels; ma_format format; getFileInfo(filepath, &sampleRate, &channels, &format); bool sameFormat = (currentDecoder == NULL || (format == currentDecoder->outputFormat && channels == currentDecoder->outputChannels && sampleRate == currentDecoder->outputSampleRate)); if (!sameFormat) { return 0; } uninitPreviousDecoder(); ma_decoder *decoder = (ma_decoder *)malloc(sizeof(ma_decoder)); ma_result result = ma_decoder_init_file(filepath, NULL, decoder); if (result != MA_SUCCESS) { free(decoder); return -1; } setNextDecoder(decoder); if (currentDecoder != NULL && decoder != NULL) ma_data_source_set_next(currentDecoder, decoder); return 0; } int prepareNextM4aDecoder(char *filepath) { m4a_decoder *currentDecoder; if (m4aDecoderIndex == -1) { currentDecoder = getFirstM4aDecoder(); } else { currentDecoder = m4aDecoders[m4aDecoderIndex]; } ma_uint32 sampleRate; ma_uint32 channels; ma_format format; ma_channel channelMap[MA_MAX_CHANNELS]; m4a_decoder_get_data_format(currentDecoder, &format, &channels, &sampleRate, channelMap, MA_MAX_CHANNELS); uninitPreviousM4aDecoder(); m4a_decoder *decoder = (m4a_decoder *)malloc(sizeof(m4a_decoder)); ma_result result = m4a_decoder_init_file(filepath, NULL, NULL, decoder); if (result != MA_SUCCESS) return -1; ma_format nformat; ma_uint32 nchannels; ma_uint32 nsampleRate; ma_channel nchannelMap[MA_MAX_CHANNELS]; m4a_decoder_get_data_format(decoder, &nformat, &nchannels, &nsampleRate, nchannelMap, MA_MAX_CHANNELS); bool sameFormat = (currentDecoder == NULL || (format == nformat && channels == nchannels && sampleRate == nsampleRate)); if (!sameFormat) { m4a_decoder_uninit(decoder, NULL); free(decoder); return -1; } m4a_decoder *first = getFirstM4aDecoder(); if (first != NULL) { decoder->pReadSeekTellUserData = (AudioData *)first->pReadSeekTellUserData; } decoder->format = nformat; decoder->onRead = m4a_read_pcm_frames_wrapper; decoder->onSeek = m4a_seek_to_pcm_frame_wrapper; decoder->onTell = m4a_get_cursor_in_pcm_frames_wrapper; decoder->cursor = 0; setNextM4aDecoder(decoder); if (currentDecoder != NULL) ma_data_source_set_next(currentDecoder, decoder); return 0; } int prepareNextOpusDecoder(char *filepath) { ma_libopus *currentDecoder; if (opusDecoderIndex == -1) { currentDecoder = getFirstOpusDecoder(); } else { currentDecoder = opusDecoders[decoderIndex]; } ma_uint32 sampleRate; ma_uint32 channels; ma_format format; ma_channel channelMap[MA_MAX_CHANNELS]; ma_libopus_get_data_format(currentDecoder, &format, &channels, &sampleRate, channelMap, MA_MAX_CHANNELS); uninitPreviousOpusDecoder(); ma_libopus *decoder = (ma_libopus *)malloc(sizeof(ma_libopus)); ma_result result = ma_libopus_init_file(filepath, NULL, NULL, decoder); if (result != MA_SUCCESS) return -1; ma_format nformat; ma_uint32 nchannels; ma_uint32 nsampleRate; ma_channel nchannelMap[MA_MAX_CHANNELS]; ma_libopus_get_data_format(decoder, &nformat, &nchannels, &nsampleRate, nchannelMap, MA_MAX_CHANNELS); bool sameFormat = (currentDecoder == NULL || (format == nformat && channels == nchannels && sampleRate == nsampleRate)); if (!sameFormat) { ma_libopus_uninit(decoder, NULL); free(decoder); return -1; } ma_libopus *first = getFirstOpusDecoder(); if (first != NULL) { decoder->pReadSeekTellUserData = (AudioData *)first->pReadSeekTellUserData; } decoder->format = nformat; decoder->onRead = ma_libopus_read_pcm_frames_wrapper; decoder->onSeek = ma_libopus_seek_to_pcm_frame_wrapper; decoder->onTell = ma_libopus_get_cursor_in_pcm_frames_wrapper; setNextOpusDecoder(decoder); if (currentDecoder != NULL) ma_data_source_set_next(currentDecoder, decoder); return 0; } int getBufferSize() { return bufSize; } void setBufferSize(int value) { bufSize = value; } void initAudioBuffer() { if (audioBuffer == NULL) { audioBuffer = malloc(sizeof(ma_int32) * MAX_BUFFER_SIZE); if (audioBuffer == NULL) { // Memory allocation failed return; } } } ma_int32 *getAudioBuffer() { return audioBuffer; } void setAudioBuffer(ma_int32 *buf) { audioBuffer = buf; } void resetAudioBuffer() { memset(audioBuffer, 0, sizeof(float) * MAX_BUFFER_SIZE); } void freeAudioBuffer() { if (audioBuffer != NULL) { free(audioBuffer); audioBuffer = NULL; } } bool isRepeatEnabled() { return repeatEnabled; } void setRepeatEnabled(bool value) { repeatEnabled = value; } bool isShuffleEnabled() { return shuffleEnabled; } void setShuffleEnabled(bool value) { shuffleEnabled = value; } bool isSkipToNext() { return skipToNext; } void setSkipToNext(bool value) { skipToNext = value; } double getSeekElapsed() { return seekElapsed; } double getPercentageElapsed() { return elapsedSeconds / duration; } void setSeekElapsed(double value) { seekElapsed = value; } bool isEOFReached() { return atomic_load(&EOFReached); } void setEOFReached() { atomic_store(&EOFReached, true); } void setEOFNotReached() { atomic_store(&EOFReached, false); } bool isImplSwitchReached() { return atomic_load(&switchReached) ? true : false; } void setImplSwitchReached() { atomic_store(&switchReached, true); } void setImplSwitchNotReached() { atomic_store(&switchReached, false); } bool isPlaying() { return ma_device_is_started(&device); } bool isPlaybackDone() { if (isEOFReached()) { return true; } else { return false; } } float getSeekPercentage() { return seekPercent; } bool isSeekRequested() { return seekRequested; } void setSeekRequested(bool value) { seekRequested = value; } void seekPercentage(float percent) { seekPercent = percent; seekRequested = true; } void resumePlayback() { // if this was unpaused with no song loaded if (audioData.restart) { audioData.endOfListReached = false; } if (!ma_device_is_started(&device)) { ma_device_start(&device); } paused = false; stopped = false; if (appState.currentView != SONG_VIEW) { refresh = true; } } void stopPlayback() { if (ma_device_is_started(&device)) { ma_device_stop(&device); } stopped = true; if (appState.currentView != SONG_VIEW) { refresh = true; } } void pausePlayback() { if (ma_device_is_started(&device)) { ma_device_stop(&device); } paused = true; if (appState.currentView != SONG_VIEW) { refresh = true; } } void cleanupPlaybackDevice() { ma_device_stop(&device); while (ma_device_get_state(&device) != ma_device_state_stopped && ma_device_get_state(&device) != ma_device_state_uninitialized) { c_sleep(100); } ma_device_uninit(&device); } void clearCurrentTrack() { if (ma_device_is_started(&device)) { // Stop the device (which stops playback) ma_device_stop(&device); } resetDecoders(); resetVorbisDecoders(); resetOpusDecoders(); resetM4aDecoders(); ma_data_source_set_next(currentDecoder, NULL); } void togglePausePlayback() { if (ma_device_is_started(&device)) { pausePlayback(); } else if (isPaused() || isStopped()) { resumePlayback(); } } bool isPaused() { return paused; } bool isStopped() { return stopped; } pthread_mutex_t deviceMutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t deviceStopped = PTHREAD_COND_INITIALIZER; void resetDevice() { pthread_mutex_lock(&deviceMutex); if (ma_device_get_state(&device) == ma_device_state_started) ma_device_stop(&device); while (ma_device_get_state(&device) == ma_device_state_started) { pthread_cond_wait(&deviceStopped, &deviceMutex); } pthread_mutex_unlock(&deviceMutex); ma_device_uninit(&device); } ma_device *getDevice() { return &device; } bool hasBuiltinDecoder(char *filePath) { char *extension = strrchr(filePath, '.'); return (extension != NULL && (strcasecmp(extension, ".wav") == 0 || strcasecmp(extension, ".flac") == 0 || strcasecmp(extension, ".mp3") == 0)); } void setCurrentFileIndex(AudioData *pAudioData, int index) { pthread_mutex_lock(&switchMutex); pAudioData->currentFileIndex = index; pthread_mutex_unlock(&switchMutex); } void activateSwitch(AudioData *pAudioData) { setSkipToNext(false); if (!isRepeatEnabled()) { pthread_mutex_lock(&switchMutex); pAudioData->currentFileIndex = 1 - pAudioData->currentFileIndex; // Toggle between 0 and 1 pthread_mutex_unlock(&switchMutex); } pAudioData->switchFiles = true; } bool isValidFilepath(const char *path) { if (path == NULL || strlen(path) == 0 || strlen(path) >= PATH_MAX) { return false; } // Check if the path can be accessed (exists) if (access(path, F_OK) != 0) { return false; // Path doesn't exist or is inaccessible } return true; // Valid path that exists } gint64 getLengthInMicroSec(double duration) { return floor(llround(duration * G_USEC_PER_SEC)); } #ifdef USE_LIBNOTIFY void onNotificationClosed(NotifyNotification *notification, gpointer user_data) { (void)user_data; g_object_unref(notification); previous_notification = NULL; } void removeBlacklistedChars(const char *input, const char *blacklist, char *output, size_t output_size) { if (!input || !blacklist || !output || output_size == 0) { return; } const char *in_ptr = input; char *out_ptr = output; size_t chars_copied = 0; while (*in_ptr && chars_copied < output_size - 1) { // Copy characters not in blacklist if (!strchr(blacklist, *in_ptr)) { *out_ptr++ = *in_ptr; chars_copied++; } in_ptr++; } *out_ptr = '\0'; } #define NOTIFICATION_INTERVAL_MICROSECONDS 500000 // 0.5 seconds struct timeval lastNotificationTime = {0, 0}; static char sanitizedArtist[512]; static char sanitizedTitle[512]; int canShowNotification() { struct timeval now; gettimeofday(&now, NULL); long seconds = now.tv_sec - lastNotificationTime.tv_sec; long microseconds = now.tv_usec - lastNotificationTime.tv_usec; long elapsed = seconds * 1000000 + microseconds; // Total elapsed time in microseconds if (elapsed >= NOTIFICATION_INTERVAL_MICROSECONDS) { lastNotificationTime = now; return 1; } return 0; } void ensureNonEmpty(char *str) { if (str == NULL || str[0] == '\0') { str[0] = ' '; str[1] = '\0'; } } int displaySongNotification(const char *artist, const char *title, const char *cover) { if (!allowNotifications || !canShowNotification() || !notify_is_initted()) { return 0; } const char *blacklist = "&;`|*~<>^()[]{}$\\\""; removeBlacklistedChars(artist, blacklist, sanitizedArtist, sizeof(sanitizedArtist)); removeBlacklistedChars(title, blacklist, sanitizedTitle, sizeof(sanitizedTitle)); ensureNonEmpty(sanitizedArtist); ensureNonEmpty(sanitizedTitle); int coverExists = isValidFilepath(cover); if (previous_notification == NULL) { previous_notification = notify_notification_new( sanitizedArtist, sanitizedTitle, coverExists ? cover : NULL ); g_signal_connect(previous_notification, "closed", G_CALLBACK(onNotificationClosed), NULL); } else { notify_notification_update( previous_notification, sanitizedArtist, sanitizedTitle, coverExists ? cover : NULL ); } GError *error = NULL; if (!notify_notification_show(previous_notification, &error)) { if (error != NULL) { fprintf(stderr, "Failed to show notification: %s\n", error->message); g_error_free(error); } } return 0; } #endif void executeSwitch(AudioData *pAudioData) { pAudioData->switchFiles = false; switchDecoder(); switchOpusDecoder(); switchM4aDecoder(); switchVorbisDecoder(); pAudioData->pUserData->currentSongData = (pAudioData->currentFileIndex == 0) ? pAudioData->pUserData->songdataA : pAudioData->pUserData->songdataB; pAudioData->totalFrames = 0; pAudioData->currentPCMFrame = 0; setSeekElapsed(0.0); setEOFReached(); } int getCurrentVolume() { return soundVolume; } int extractPercentage(char *str) { int volume = -1; char *percentSign = strchr(str, '%'); if (percentSign != NULL) { // Find the start of the number before the '%' while (percentSign > str && *(percentSign - 1) == ' ') percentSign--; while (percentSign > str && *(percentSign - 1) >= '0' && *(percentSign - 1) <= '9') percentSign--; volume = atoi(percentSign); } return volume; } int getSystemVolume() { FILE *fp; char command_str[1000]; char buf[256]; int currentVolume = -1; // Get the default sink's name snprintf(command_str, sizeof(command_str), "pactl info | grep 'Default Sink' | awk '{print $3}'"); fp = popen(command_str, "r"); if (fp != NULL) { if (fgets(buf, sizeof(buf), fp) != NULL) { buf[strcspn(buf, "\n")] = '\0'; snprintf(command_str, sizeof(command_str), "pactl get-sink-volume %s", buf); FILE *volume_fp = popen(command_str, "r"); if (volume_fp != NULL) { while (fgets(buf, sizeof(buf), volume_fp) != NULL) { int tempVolume = extractPercentage(buf); if (tempVolume != -1) { currentVolume = tempVolume; break; } } pclose(volume_fp); } } pclose(fp); } // ALSA if (currentVolume == -1) { snprintf(command_str, sizeof(command_str), "amixer get Master"); fp = popen(command_str, "r"); if (fp != NULL) { while (fgets(buf, sizeof(buf), fp) != NULL) { int tempVolume = extractPercentage(buf); if (tempVolume != -1) { currentVolume = tempVolume; break; } } pclose(fp); } } return currentVolume; } void setVolume(int volume) { if (volume > 100) { volume = 100; } else if (volume < 0) { volume = 0; } soundVolume = volume; ma_device_set_master_volume(getDevice(), (float)volume / 100); } int adjustVolumePercent(int volumeChange) { int sysVol = getSystemVolume(); if (sysVol == 0) return 0; int step = 100 / sysVol * 5; int relativeVolChange = volumeChange / 5 * step; soundVolume += relativeVolChange; setVolume(soundVolume); return 0; } ma_uint64 lastCursor = 0; void m4a_read_pcm_frames(ma_data_source *pDataSource, void *pFramesOut, ma_uint64 frameCount, ma_uint64 *pFramesRead) { m4a_decoder *m4a = (m4a_decoder *)pDataSource; AudioData *pAudioData = (AudioData *)m4a->pReadSeekTellUserData; ma_uint64 framesRead = 0; while (framesRead < frameCount) { if (doQuit) return; if (isImplSwitchReached()) return; if (pthread_mutex_trylock(&dataSourceMutex) != 0) { return; } // Check if a file switch is required if (pAudioData->switchFiles) { executeSwitch(pAudioData); pthread_mutex_unlock(&dataSourceMutex); break; // Exit the loop after the file switch } if (getCurrentImplementationType() != M4A && !isSkipToNext()) { pthread_mutex_unlock(&dataSourceMutex); return; } m4a_decoder *decoder = getCurrentM4aDecoder(); if (pAudioData->totalFrames == 0) ma_data_source_get_length_in_pcm_frames(decoder, &pAudioData->totalFrames); // Check if seeking is requested if (isSeekRequested()) { ma_uint64 totalFrames = 0; m4a_decoder_get_length_in_pcm_frames(decoder, &totalFrames); ma_uint64 seekPercent = getSeekPercentage(); if (seekPercent >= 100.0) seekPercent = 100.0; ma_uint64 targetFrame = (totalFrames * seekPercent) / 100 - 1; // Remove one frame or we get invalid args if we send in totalframes // Set the read pointer for the decoder ma_result seekResult = m4a_decoder_seek_to_pcm_frame(decoder, targetFrame); if (seekResult != MA_SUCCESS) { // Handle seek error setSeekRequested(false); pthread_mutex_unlock(&dataSourceMutex); return; } setSeekRequested(false); // Reset seek flag } // Read from the current decoder ma_uint64 framesToRead = 0; ma_result result; ma_uint64 remainingFrames = frameCount - framesRead; m4a_decoder *firstDecoder = getFirstM4aDecoder(); ma_uint64 cursor = 0; if (firstDecoder == NULL) { pthread_mutex_unlock(&dataSourceMutex); return; } result = ma_data_source_read_pcm_frames(firstDecoder, (ma_int32 *)pFramesOut + framesRead * pAudioData->channels, remainingFrames, &framesToRead); ma_data_source_get_cursor_in_pcm_frames(decoder, &cursor); if (((cursor != 0 && cursor == lastCursor) || framesToRead == 0 || isSkipToNext() || result != MA_SUCCESS) && !isEOFReached()) { activateSwitch(pAudioData); pthread_mutex_unlock(&dataSourceMutex); continue; } lastCursor = cursor; framesRead += framesToRead; setBufferSize(framesToRead); pthread_mutex_unlock(&dataSourceMutex); } ma_int32 *audioBuffer = getAudioBuffer(); if (audioBuffer == NULL) { audioBuffer = malloc(sizeof(ma_int32) * MAX_BUFFER_SIZE); if (audioBuffer == NULL) { // Memory allocation failed return; } } // No format conversion needed, just copy the audio samples memcpy(audioBuffer, pFramesOut, sizeof(ma_int32) * framesRead); setAudioBuffer(audioBuffer); if (pFramesRead != NULL) { *pFramesRead = framesRead; } } void m4a_on_audio_frames(ma_device *pDevice, void *pFramesOut, const void *pFramesIn, ma_uint32 frameCount) { AudioData *pDataSource = (AudioData *)pDevice->pUserData; ma_uint64 framesRead = 0; m4a_read_pcm_frames(&pDataSource->base, pFramesOut, frameCount, &framesRead); (void)pFramesIn; } void opus_read_pcm_frames(ma_data_source *pDataSource, void *pFramesOut, ma_uint64 frameCount, ma_uint64 *pFramesRead) { ma_libopus *opus = (ma_libopus *)pDataSource; AudioData *pAudioData = (AudioData *)opus->pReadSeekTellUserData; ma_uint64 framesRead = 0; while (framesRead < frameCount) { if (doQuit) return; if (isImplSwitchReached()) return; if (pthread_mutex_trylock(&dataSourceMutex) != 0) { return; } // Check if a file switch is required if (pAudioData->switchFiles) { executeSwitch(pAudioData); pthread_mutex_unlock(&dataSourceMutex); break; // Exit the loop after the file switch } if (getCurrentImplementationType() != OPUS && !isSkipToNext()) { pthread_mutex_unlock(&dataSourceMutex); return; } ma_libopus *decoder = getCurrentOpusDecoder(); if (pAudioData->totalFrames == 0) ma_data_source_get_length_in_pcm_frames(decoder, &pAudioData->totalFrames); // Check if seeking is requested if (isSeekRequested()) { ma_uint64 totalFrames = 0; ma_libopus_get_length_in_pcm_frames(decoder, &totalFrames); ma_uint64 seekPercent = getSeekPercentage(); if (seekPercent >= 100.0) seekPercent = 100.0; ma_uint64 targetFrame = (totalFrames * seekPercent) / 100 - 1; // Remove one frame or we get invalid args if we send in totalframes // Set the read pointer for the decoder ma_result seekResult = ma_libopus_seek_to_pcm_frame(decoder, targetFrame); if (seekResult != MA_SUCCESS) { // Handle seek error setSeekRequested(false); pthread_mutex_unlock(&dataSourceMutex); return; } setSeekRequested(false); // Reset seek flag } // Read from the current decoder ma_uint64 framesToRead = 0; ma_result result; ma_uint64 remainingFrames = frameCount - framesRead; ma_libopus *firstDecoder = getFirstOpusDecoder(); ma_uint64 cursor = 0; if (firstDecoder == NULL) { pthread_mutex_unlock(&dataSourceMutex); return; } result = ma_data_source_read_pcm_frames(firstDecoder, (ma_int32 *)pFramesOut + framesRead * pAudioData->channels, remainingFrames, &framesToRead); ma_data_source_get_cursor_in_pcm_frames(decoder, &cursor); if (((cursor != 0 && cursor >= pAudioData->totalFrames) || framesToRead == 0 || isSkipToNext() || result != MA_SUCCESS) && !isEOFReached()) { activateSwitch(pAudioData); pthread_mutex_unlock(&dataSourceMutex); continue; } framesRead += framesToRead; setBufferSize(framesToRead); pthread_mutex_unlock(&dataSourceMutex); } ma_int32 *audioBuffer = getAudioBuffer(); if (audioBuffer == NULL) { audioBuffer = malloc(sizeof(ma_int32) * MAX_BUFFER_SIZE); if (audioBuffer == NULL) { // Memory allocation failed return; } } // No format conversion needed, just copy the audio samples memcpy(audioBuffer, pFramesOut, sizeof(ma_int32) * framesRead); setAudioBuffer(audioBuffer); if (pFramesRead != NULL) { *pFramesRead = framesRead; } } void opus_on_audio_frames(ma_device *pDevice, void *pFramesOut, const void *pFramesIn, ma_uint32 frameCount) { AudioData *pDataSource = (AudioData *)pDevice->pUserData; ma_uint64 framesRead = 0; opus_read_pcm_frames(&pDataSource->base, pFramesOut, frameCount, &framesRead); (void)pFramesIn; } void vorbis_read_pcm_frames(ma_data_source *pDataSource, void *pFramesOut, ma_uint64 frameCount, ma_uint64 *pFramesRead) { ma_libvorbis *vorbis = (ma_libvorbis *)pDataSource; AudioData *pAudioData = (AudioData *)vorbis->pReadSeekTellUserData; ma_uint64 framesRead = 0; while (framesRead < frameCount) { if (doQuit) return; if (isImplSwitchReached()) return; if (pthread_mutex_trylock(&dataSourceMutex) != 0) { return; } // Check if a file switch is required if (pAudioData->switchFiles) { executeSwitch(pAudioData); pthread_mutex_unlock(&dataSourceMutex); break; // Exit the loop after the file switch } ma_libvorbis *decoder = getCurrentVorbisDecoder(); if ((getCurrentImplementationType() != VORBIS && !isSkipToNext()) || (decoder == NULL)) { pthread_mutex_unlock(&dataSourceMutex); return; } if (isSeekRequested()) { // disabled for ogg vorbis setSeekRequested(false); } // Read from the current decoder ma_uint64 framesToRead = 0; ma_result result; ma_uint64 remainingFrames = frameCount - framesRead; ma_libvorbis *firstDecoder = getFirstVorbisDecoder(); if (firstDecoder == NULL) { pthread_mutex_unlock(&dataSourceMutex); return; } result = ma_data_source_read_pcm_frames(firstDecoder, (ma_int32 *)pFramesOut + framesRead * pAudioData->channels, remainingFrames, &framesToRead); if ((getPercentageElapsed() >= 1.0 || isSkipToNext() || result != MA_SUCCESS) && !isEOFReached()) { activateSwitch(pAudioData); pthread_mutex_unlock(&dataSourceMutex); continue; } framesRead += framesToRead; setBufferSize(framesToRead); pthread_mutex_unlock(&dataSourceMutex); } ma_int32 *audioBuffer = getAudioBuffer(); if (audioBuffer == NULL) { audioBuffer = malloc(sizeof(ma_int32) * MAX_BUFFER_SIZE); if (audioBuffer == NULL) { // Memory allocation failed return; } } // No format conversion needed, just copy the audio samples memcpy(audioBuffer, pFramesOut, sizeof(ma_int32) * framesRead); setAudioBuffer(audioBuffer); if (pFramesRead != NULL) { *pFramesRead = framesRead; } } void vorbis_on_audio_frames(ma_device *pDevice, void *pFramesOut, const void *pFramesIn, ma_uint32 frameCount) { AudioData *pDataSource = (AudioData *)pDevice->pUserData; ma_uint64 framesRead = 0; vorbis_read_pcm_frames(&pDataSource->base, pFramesOut, frameCount, &framesRead); (void)pFramesIn; } kew-2.8.2/src/soundcommon.h000066400000000000000000000175771467402032100156320ustar00rootroot00000000000000#ifndef SOUND_COMMON_H #define SOUND_COMMON_H #include #include #include #include #include #include #include #include #include #include "m4a.h" #include #include #include #include "file.h" #include "utils.h" #ifdef USE_LIBNOTIFY #include #endif #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif #ifndef MAX_BUFFER_SIZE #define MAX_BUFFER_SIZE 4800 #endif #ifndef TAGSETTINGS_STRUCT #define TAGSETTINGS_STRUCT typedef struct { char title[256]; char artist[256]; char album_artist[256]; char album[256]; char date[256]; } TagSettings; #endif #ifndef SONGDATA_STRUCT #define SONGDATA_STRUCT typedef struct { gchar *trackId; char filePath[MAXPATHLEN]; char coverArtPath[MAXPATHLEN]; unsigned char red; unsigned char green; unsigned char blue; TagSettings *metadata; FIBITMAP *cover; double duration; bool hasErrors; } SongData; #endif #ifndef USERDATA_STRUCT #define USERDATA_STRUCT typedef struct { SongData *songdataA; SongData *songdataB; bool songdataADeleted; bool songdataBDeleted; SongData *currentSongData; ma_uint32 currentPCMFrame; } UserData; #endif #ifndef AUDIODATA_STRUCT #define AUDIODATA_STRUCT typedef struct { ma_data_source_base base; UserData *pUserData; ma_format format; ma_uint32 channels; ma_uint32 sampleRate; ma_uint32 currentPCMFrame; bool switchFiles; int currentFileIndex; ma_uint64 totalFrames; bool endOfListReached; bool restart; } AudioData; #endif #ifndef KEYVALUEPAIR_STRUCT #define KEYVALUEPAIR_STRUCT typedef struct { char *key; char *value; } KeyValuePair; #endif #ifndef APPSETTINGS_STRUCT typedef struct { char path[MAXPATHLEN]; char coverEnabled[2]; char coverAnsi[2]; char useProfileColors[2]; char visualizerEnabled[2]; char visualizerHeight[6]; char togglePlaylist[6]; char toggleBindings[6]; char volumeUp[6]; char volumeUpAlt[6]; char volumeDown[6]; char previousTrackAlt[6]; char nextTrackAlt[6]; char scrollUpAlt[6]; char scrollDownAlt[6]; char switchNumberedSong[6]; char switchNumberedSongAlt[6]; char switchNumberedSongAlt2[6]; char togglePause[6]; char toggleColorsDerivedFrom[6]; char toggleVisualizer[6]; char toggleAscii[6]; char toggleRepeat[6]; char toggleShuffle[6]; char seekBackward[6]; char seekForward[6]; char savePlaylist[6]; char addToMainPlaylist[6]; char updateLibrary[6]; char quit[6]; char hardQuit[6]; char hardSwitchNumberedSong[6]; char hardPlayPause[6]; char hardPrev[6]; char hardNext[6]; char hardScrollUp[6]; char hardScrollDown[6]; char hardShowPlaylist[6]; char hardShowPlaylistAlt[6]; char showPlaylistAlt[6]; char hardShowKeys[6]; char hardShowKeysAlt[6]; char showKeysAlt[6]; char hardEndOfPlaylist[6]; char hardShowLibrary[6]; char hardShowLibraryAlt[6]; char showLibraryAlt[6]; char hardShowSearch[6]; char hardShowSearchAlt[6]; char showSearchAlt[6]; char hardShowTrack[6]; char hardShowTrackAlt[6]; char showTrackAlt[6]; char hardNextPage[6]; char hardPrevPage[6]; char hardRemove[6]; char hardRemove2[6]; char lastVolume[12]; char allowNotifications[2]; char color[2]; char artistColor[2]; char enqueuedColor[2]; char titleColor[2]; char hideLogo[2]; char hideHelp[2]; char cacheLibrary[6]; char tabNext[6]; } AppSettings; #endif enum AudioImplementation { PCM, BUILTIN, VORBIS, OPUS, M4A, NONE }; typedef enum { SONG_VIEW, KEYBINDINGS_VIEW, PLAYLIST_VIEW, LIBRARY_VIEW, SEARCH_VIEW } ViewState; typedef struct { ViewState currentView; } AppState; #ifdef USE_LIBNOTIFY extern NotifyNotification *previous_notification; #endif extern AppState appState; extern bool allowNotifications; extern volatile bool refresh; extern AudioData audioData; extern double duration; extern bool doQuit; extern double elapsedSeconds; extern bool hasSilentlySwitched; extern pthread_mutex_t dataSourceMutex; extern pthread_mutex_t switchMutex; enum AudioImplementation getCurrentImplementationType(); void setCurrentImplementationType(enum AudioImplementation value); int getBufferSize(); void setBufferSize(int value); void setPlayingStatus(bool playing); bool isPlaying(); ma_decoder *getFirstDecoder(); ma_decoder *getCurrentBuiltinDecoder(); ma_decoder *getPreviousDecoder(); ma_format getCurrentFormat(); void resetDecoders(); ma_libopus *getCurrentOpusDecoder(); void resetOpusDecoders(); m4a_decoder *getCurrentM4aDecoder(); m4a_decoder *getFirstM4aDecoder(); ma_libopus *getFirstOpusDecoder(); ma_libvorbis *getFirstVorbisDecoder(); void getVorbisFileInfo(const char *filename, ma_format *format, ma_uint32*channels, ma_uint32 *sampleRate, ma_channel *channelMap); void getM4aFileInfo(const char *filename, ma_format *format, ma_uint32*channels, ma_uint32 *sampleRate, ma_channel *channelMap); void getOpusFileInfo(const char *filename, ma_format *format, ma_uint32*channels, ma_uint32 *sampleRate, ma_channel *channelMap); ma_libvorbis *getCurrentVorbisDecoder(); void switchVorbisDecoder(); int prepareNextOpusDecoder(char *filepath); int prepareNextOpusDecoder(char *filepath); int prepareNextVorbisDecoder(char *filepath); int prepareNextM4aDecoder(char *filepath); void resetVorbisDecoders(); void resetM4aDecoders(); ma_libvorbis *getFirstVorbisDecoder(); void getFileInfo(const char* filename, ma_uint32* sampleRate, ma_uint32* channels, ma_format* format); void initAudioBuffer(); ma_int32 *getAudioBuffer(); void setAudioBuffer(ma_int32 *buf); void resetAudioBuffer(); void freeAudioBuffer(); bool isRepeatEnabled(); void setRepeatEnabled(bool value); bool isShuffleEnabled(); void setShuffleEnabled(bool value); bool isSkipToNext(); void setSkipToNext(bool value); double getSeekElapsed(); void setSeekElapsed(double value); bool isEOFReached(); void setEOFReached(); void setEOFNotReached(); bool isImplSwitchReached(); void setImplSwitchReached(); void setImplSwitchNotReached(); bool isPlaybackDone(); float getSeekPercentage(); double getPercentageElapsed(); bool isSeekRequested(); void setSeekRequested(bool value); void seekPercentage(float percent); void resumePlayback(); void stopPlayback(); void pausePlayback(); void cleanupPlaybackDevice(); void togglePausePlayback(); bool isPaused(); bool isStopped(); void resetDevice(); ma_device *getDevice(); bool hasBuiltinDecoder(char *filePath); void setCurrentFileIndex(AudioData *pAudioData, int index); void activateSwitch(AudioData *pPCMDataSource); void executeSwitch(AudioData *pPCMDataSource); gint64 getLengthInMicroSec(double duration); int displaySongNotification(const char *artist, const char *title, const char *cover); int getCurrentVolume(void); void setVolume(int volume); int adjustVolumePercent(int volumeChange); void m4a_on_audio_frames(ma_device *pDevice, void *pFramesOut, const void *pFramesIn, ma_uint32 frameCount); void opus_on_audio_frames(ma_device *pDevice, void *pFramesOut, const void *pFramesIn, ma_uint32 frameCount); void vorbis_on_audio_frames(ma_device *pDevice, void *pFramesOut, const void *pFramesIn, ma_uint32 frameCount); void logTime(const char *message); void clearCurrentTrack(); #endif kew-2.8.2/src/term.c000066400000000000000000000171431467402032100142200ustar00rootroot00000000000000#include "term.h" /* term.c This file should contain only simple utility functions related to the terminal. They should work independently and be as decoupled from the application as possible. */ volatile sig_atomic_t resizeFlag = 0; void setTextColor(int color) { /* - 0: Black - 1: Red - 2: Green - 3: Yellow - 4: Blue - 5: Magenta - 6: Cyan - 7: White */ printf("\033[0;3%dm", color); } void setTextColorRGB(int r, int g, int b) { printf("\033[0;38;2;%03u;%03u;%03um", r, g, b); } void getTermSize(int *width, int *height) { struct winsize w; ioctl(STDOUT_FILENO, TIOCGWINSZ, &w); *height = (int)w.ws_row; *width = (int)w.ws_col; } void setNonblockingMode() { struct termios ttystate; tcgetattr(STDIN_FILENO, &ttystate); ttystate.c_lflag &= ~ICANON; ttystate.c_cc[VMIN] = 0; ttystate.c_cc[VTIME] = 0; tcsetattr(STDIN_FILENO, TCSANOW, &ttystate); } void restoreTerminalMode() { struct termios ttystate; tcgetattr(STDIN_FILENO, &ttystate); ttystate.c_lflag |= ICANON; tcsetattr(STDIN_FILENO, TCSANOW, &ttystate); } void saveCursorPosition() { printf("\033[s"); } void restoreCursorPosition() { printf("\033[u"); } void setDefaultTextColor() { printf("\033[0m"); } int isInputAvailable() { fd_set fds; FD_ZERO(&fds); FD_SET(STDIN_FILENO, &fds); struct timeval tv; tv.tv_sec = 0; tv.tv_usec = 0; int ret = select(STDIN_FILENO + 1, &fds, NULL, NULL, &tv); if (ret < 0) { return 0; } int result = (ret > 0) && (FD_ISSET(STDIN_FILENO, &fds)); return result; } void hideCursor() { printf("\033[?25l"); fflush(stdout); } void showCursor() { printf("\033[?25h"); fflush(stdout); } void resetConsole() { // Print ANSI escape codes to reset terminal, clear screen, and move cursor to top-left printf("\033\143"); // Reset to Initial State (RIS) printf("\033[3J"); // Clear scrollback buffer printf("\033[H\033[J"); // Move cursor to top-left and clear screen fflush(stdout); } void clearRestOfScreen() { printf("\033[J"); } void clearScreen() { printf("\033[3J"); // Clear scrollback buffer printf("\033[H\033[J"); // Move cursor to top-left and clear screen } void enableScrolling() { printf("\033[?7h"); } void handleResize(int sig) { (void)sig; resizeFlag = 1; } void resetResizeFlag(int sig) { (void)sig; resizeFlag = 0; } void initResize() { signal(SIGWINCH, handleResize); struct sigaction sa; sa.sa_handler = resetResizeFlag; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGALRM, &sa, NULL); } void disableInputBuffering(void) { struct termios term; tcgetattr(STDIN_FILENO, &term); term.c_lflag &= ~(ICANON | ECHO); tcsetattr(STDIN_FILENO, TCSAFLUSH, &term); } void enableInputBuffering() { struct termios term; tcgetattr(STDIN_FILENO, &term); term.c_lflag |= ICANON | ECHO; tcsetattr(STDIN_FILENO, TCSAFLUSH, &term); } void cursorJump(int numRows) { printf("\033[%dA", numRows); printf("\033[0m"); } void cursorJumpDown(int numRows) { printf("\033[%dB", numRows); } int readInputSequence_old(char *seq, size_t seqSize) { char c; ssize_t bytesRead; seq[0] = '\0'; bytesRead = read(STDIN_FILENO, &c, 1); if (bytesRead <= 0) { return 0; } // If it's not an escape character, return it as a single-character sequence if (c != '\x1b') { seq[0] = c; seq[1] = '\0'; return 1; } // Read additional characters if (seqSize < 3) return 0; bytesRead = read(STDIN_FILENO, seq, 2); if ((bytesRead <= 0) & (seq[0] == '\0')) { seq[0] = '\0'; return 0; } seq[bytesRead] = '\0'; return bytesRead; } int readInputSequence(char *seq, size_t seqSize) { char c; ssize_t bytesRead; seq[0] = '\0'; bytesRead = read(STDIN_FILENO, &c, 1); if (bytesRead <= 0) { return 0; } // If it's a single-byte ASCII character, return it if ((c & 0x80) == 0) { seq[0] = c; seq[1] = '\0'; return 1; } // Determine the length of the UTF-8 sequence int additionalBytes; if ((c & 0xE0) == 0xC0) { additionalBytes = 1; // 2-byte sequence } else if ((c & 0xF0) == 0xE0) { additionalBytes = 2; // 3-byte sequence } else if ((c & 0xF8) == 0xF0) { additionalBytes = 3; // 4-byte sequence } else { // Invalid UTF-8 start byte return 0; } // Ensure there's enough space in the buffer if ((size_t)additionalBytes + 1 >= seqSize) { return 0; } // Read the remaining bytes of the UTF-8 sequence seq[0] = c; bytesRead = read(STDIN_FILENO, &seq[1], additionalBytes); if (bytesRead != additionalBytes) { return 0; } seq[additionalBytes + 1] = '\0'; return additionalBytes + 1; } int getIndentation(int terminalWidth) { int term_w, term_h; getTermSize(&term_w, &term_h); int indent = ((term_w - terminalWidth) / 2) + 1; return (indent > 0) ? indent : 0; } int isFunctionKey(const char *seq) { if (seq[0] == '\033') { // ESC character return 1; } return 0; } void convertControlNotationToAscii(char *input, char *output, size_t size) { if (input[0] == '^' && input[1] >= 'A' && input[1] <= 'Z') { output[0] = input[1] - 'A' + 1; // Convert ^A to Ctrl+A (ASCII 1) output[1] = '\0'; } else if (input[0] == '^' && input[1] >= '2' && input[1] <= '8') { output[0] = input[1] - '1' + 1; // Convert ^2 to Ctrl+2 (ASCII 18), etc. output[1] = '\0'; } else { strncpy(output, input, size); } } void convertAsciiToControlNotation(char input, char *output, size_t size) { if (input >= 1 && input <= 26) { // Control characters from Ctrl+A to Ctrl+Z if (size >= 3) { // Ensure there's enough space for "^A" and null terminator output[0] = '^'; output[1] = input + 'A' - 1; output[2] = '\0'; } } else if (input >= 1 && input <= 8) { // Control characters for digits and punctuation (Ctrl+2 to Ctrl+8) if (size >= 3) { // Ensure there's enough space for "^2" and null terminator output[0] = '^'; output[1] = input + '1' - 1; output[2] = '\0'; } } else { if (size >= 2) { // Ensure there's enough space for the character and null terminator output[0] = input; output[1] = '\0'; } } } kew-2.8.2/src/term.h000066400000000000000000000025551467402032100142260ustar00rootroot00000000000000#ifndef TERM_H #define TERM_H #ifndef __USE_POSIX #define __USE_POSIX #endif #include #include #include #include #include #include #include #include #include #include #include #include #ifdef __GNU__ # define _BSD_SOURCE #endif extern volatile sig_atomic_t resizeFlag; void setTextColor(int color); void setTextColorRGB(int r, int g, int b); void getTermSize(int *width, int *height); int getIndentation(int terminalWidth); void setNonblockingMode(void); void restoreTerminalMode(void); void setDefaultTextColor(void); int isInputAvailable(void); void resetConsole(void); void saveCursorPosition(void); void restoreCursorPosition(void); void hideCursor(void); void showCursor(void); void clearRestOfScreen(void); void enableScrolling(void); void handleResize(int sig); void resetResizeFlag(int sig); void initResize(void); void disableInputBuffering(void); void enableInputBuffering(void); void cursorJump(int numRows); void cursorJumpDown(int numRows); void clearScreen(void); int readInputSequence(char *seq, size_t seqSize); int isFunctionKey(const char *seq); void convertControlNotationToAscii(char *input, char *output, size_t size); void convertAsciiToControlNotation(char input, char *output, size_t size); #endif kew-2.8.2/src/utils.c000066400000000000000000000213221467402032100144030ustar00rootroot00000000000000#include "utils.h" /* utils.c Utility functions for instance for replacing some standard functions with safer alternatives. */ void c_sleep(int milliseconds) { struct timespec ts; ts.tv_sec = milliseconds / 1000; ts.tv_nsec = milliseconds % 1000 * 1000000; nanosleep(&ts, NULL); } void c_usleep(int microseconds) { struct timespec ts; ts.tv_sec = microseconds / 1000000; ts.tv_nsec = microseconds % 1000; nanosleep(&ts, NULL); } void c_strcpy(char *dest, size_t dest_size, const char *src) { if (dest && dest_size > 0 && src) { size_t src_length = strlen(src); if (src_length < dest_size) { strncpy(dest, src, dest_size); } else { strncpy(dest, src, dest_size - 1); dest[dest_size - 1] = '\0'; } } } char *stringToLower(char *str) { for (int i = 0; str[i] != '\0'; i++) { str[i] = tolower(str[i]); } return str; } char *stringToUpper(char *str) { for (int i = 0; str[i] != '\0'; i++) { str[i] = toupper(str[i]); } return str; } char *c_strcasestr(const char *haystack, const char *needle) { if (!haystack || !needle) return NULL; size_t needleLen = strlen(needle); size_t haystackLen = strlen(haystack); if (needleLen > haystackLen) return NULL; for (size_t i = 0; i <= haystackLen - needleLen; i++) { size_t j; for (j = 0; j < needleLen; j++) { if (tolower(haystack[i + j]) != tolower(needle[j])) break; } if (j == needleLen) return (char *)(haystack + i); } return NULL; } int match_regex(regex_t *regex, const char *ext) { if (regex == NULL || ext == NULL) { fprintf(stderr, "Invalid arguments\n"); return 1; } regmatch_t pmatch[1]; // Array to store match results int ret = regexec(regex, ext, 1, pmatch, 0); if (ret == REG_NOMATCH) { return 1; } else if (ret == 0) { return 0; } else { char errorBuf[100]; regerror(ret, regex, errorBuf, sizeof(errorBuf)); fprintf(stderr, "Regex match failed: %s\n", errorBuf); return 1; } } void extractExtension(const char *filename, size_t numChars, char *ext) { size_t length = strlen(filename); size_t copyChars = length < numChars ? length : numChars; const char *extStart = filename + length - copyChars; strncpy(ext, extStart, copyChars); ext[copyChars] = '\0'; } int endsWith(const char *str, const char *suffix) { size_t strLength = strlen(str); size_t suffixLength = strlen(suffix); if (suffixLength > strLength) { return 0; } const char *strSuffix = str + (strLength - suffixLength); return strcmp(strSuffix, suffix) == 0; } int startsWith(const char *str, const char *prefix) { size_t strLength = strlen(str); size_t prefixLength = strlen(prefix); if (prefixLength > strLength) { return 0; } return strncmp(str, prefix, prefixLength) == 0; } void trim(char *str) { char *start = str; while (*start && isspace(*start)) { start++; } char *end = str + strlen(str) - 1; while (end > start && isspace(*end)) { end--; } *(end + 1) = '\0'; if (str != start) { memmove(str, start, end - start + 2); } } const char *getHomePath() { const char *xdgHome = getenv("XDG_HOME"); if (xdgHome == NULL) { xdgHome = getenv("HOME"); if (xdgHome == NULL) { struct passwd *pw = getpwuid(getuid()); if (pw != NULL) { char *home = (char *)malloc(MAXPATHLEN); strcpy(home, pw->pw_dir); return home; } } } return xdgHome; } char *getConfigPathOld() { char *configPath = malloc(MAXPATHLEN); if (!configPath) return NULL; const char *xdgConfig = getenv("XDG_CONFIG_HOME"); if (xdgConfig) { snprintf(configPath, MAXPATHLEN, "%s", xdgConfig); } else { const char *home = getHomePath(); if (home) { snprintf(configPath, MAXPATHLEN, "%s/.config", home); } else { struct passwd *pw = getpwuid(getuid()); if (pw) { snprintf(configPath, MAXPATHLEN, "%s", pw->pw_dir); } else { free(configPath); return NULL; } } } return configPath; } char *getConfigPath() { char *configPath = malloc(MAXPATHLEN); if (!configPath) return NULL; const char *xdgConfig = getenv("XDG_CONFIG_HOME"); if (xdgConfig) { snprintf(configPath, MAXPATHLEN, "%s/kew", xdgConfig); } else { const char *home = getHomePath(); if (home) { snprintf(configPath, MAXPATHLEN, "%s/.config/kew", home); } else { struct passwd *pw = getpwuid(getuid()); if (pw) { snprintf(configPath, MAXPATHLEN, "%s/.config/kew", pw->pw_dir); } else { free(configPath); return NULL; } } } return configPath; } int moveConfigFiles() { char *oldPath = getConfigPathOld(); char *newPath = getConfigPath(); if (!oldPath || !newPath) { free(oldPath); free(newPath); return -1; } struct stat st = {0}; if (stat(newPath, &st) == -1) { mkdir(newPath, 0700); } char oldFileLibrary[MAXPATHLEN]; char newFileLibrary[MAXPATHLEN]; char oldFileRc[MAXPATHLEN]; char newFileRc[MAXPATHLEN]; snprintf(oldFileLibrary, MAXPATHLEN, "%s/kewlibrary", oldPath); snprintf(newFileLibrary, MAXPATHLEN, "%s/kewlibrary", newPath); snprintf(oldFileRc, MAXPATHLEN, "%s/kewrc", oldPath); snprintf(newFileRc, MAXPATHLEN, "%s/kewrc", newPath); if (access(oldFileLibrary, F_OK) == 0) { rename(oldFileLibrary, newFileLibrary); } if (access(oldFileRc, F_OK) == 0) { rename(oldFileRc, newFileRc); } free(oldPath); free(newPath); return 0; } void removeUnneededChars(char *str) { if (strlen(str) < 6) { return; } int i; for (i = 0; i < 3 && str[i] != '\0' && str[i] != ' '; i++) { if (isdigit(str[i]) || str[i] == '-' || str[i] == ' ') { int j; for (j = i; str[j] != '\0'; j++) { str[j] = str[j + 1]; } str[j] = '\0'; i--; // Decrement i to re-check the current index } } i = 0; while (str[i] != '\0') { if (str[i] == '-' || str[i] == '_') { str[i] = ' '; } i++; } } void shortenString(char *str, size_t maxLength) { size_t length = strlen(str); if (length > maxLength) { str[maxLength] = '\0'; } } void printBlankSpaces(int numSpaces) { printf("%*s", numSpaces, " "); } kew-2.8.2/src/utils.h000066400000000000000000000022171467402032100144120ustar00rootroot00000000000000#ifndef UTILS_H #define UTILS_H #ifndef _POSIX_C_SOURCE #define _POSIX_C_SOURCE 200809L #endif #ifndef __USE_POSIX #define __USE_POSIX #endif #include #include #include #include #include #include #include #include #include #include #include #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif void c_sleep(int milliseconds); void c_usleep(int microseconds); void c_strcpy(char *dest, size_t dest_size, const char *src); char *stringToUpper(char *str); char *stringToLower(char *str); char *c_strcasestr(const char *haystack, const char *needle); int match_regex(regex_t *regex, const char *ext); void extractExtension(const char *filename, size_t numChars, char *ext); int endsWith(const char *str, const char *suffix); int startsWith(const char *str, const char *prefix); void trim(char *str); const char *getHomePath(); char *getConfigPathOld(); char *getConfigPath(); int moveConfigFiles(void); void removeUnneededChars(char *str); void shortenString(char *str, size_t maxLength); void printBlankSpaces(int numSpaces); #endif kew-2.8.2/src/visuals.c000066400000000000000000000307711467402032100147410ustar00rootroot00000000000000#include "visuals.h" /* visuals.c This file should contain only functions related to the spectrum visualizer. */ #ifndef MAX_BUFFER_SIZE #define MAX_BUFFER_SIZE 4800 #endif int bufferSize = 4800; int prevBufferSize = 0; float alpha = 0.2f; float lastMax = -1.0f; bool unicodeSupport = false; fftwf_complex *fftInput = NULL; fftwf_complex *fftOutput = NULL; int bufferIndex = 0; float magnitudeBuffer[MAX_BUFFER_SIZE] = {0.0f}; float lastMagnitudes[MAX_BUFFER_SIZE] = {0.0f}; float smoothedMagnitudes[MAX_BUFFER_SIZE]; float maxPossibleMagnitude = 0; int terminalSupportsUnicode() { char *locale = setlocale(LC_ALL, ""); if (locale != NULL) return 1; return 0; } void initVisuals() { unicodeSupport = false; if (terminalSupportsUnicode() > 0) unicodeSupport = true; } #define MOVING_AVERAGE_WINDOW_SIZE 2 void updateMagnitudes(int height, int width, float maxMagnitude, float *magnitudes) { if (maxMagnitude == 0.0f) { maxMagnitude = 1.0f; } // Apply moving average smoothing to magnitudes for (int i = 0; i < width; i++) { float sum = magnitudes[i]; int count = 1; // Calculate moving average using a window centered at the current frequency bin for (int j = 1; j <= MOVING_AVERAGE_WINDOW_SIZE / 2; j++) { if (i - j >= 0) { sum += magnitudes[i - j]; count++; } if (i + j < width) { sum += magnitudes[i + j]; count++; } } // Compute the smoothed magnitude by averaging smoothedMagnitudes[i] = sum / count; } // Update magnitudes array with smoothed values memcpy(magnitudes, smoothedMagnitudes, width * sizeof(float)); // Apply adaptive decay factor to the smoothed magnitudes float exponent = 1.0f; for (int i = 0; i < width; i++) { float normalizedMagnitude = magnitudes[i] / maxMagnitude; normalizedMagnitude = fminf(normalizedMagnitude, 1.0f); float scaledMagnitude = powf(normalizedMagnitude, exponent) * height; // Adaptive decay based on magnitude float decreaseFactor = 0.8f + 0.2f * (normalizedMagnitude); float decayedMagnitude = lastMagnitudes[i] * decreaseFactor; if (scaledMagnitude < decayedMagnitude) { magnitudes[i] = decayedMagnitude; } else { magnitudes[i] = scaledMagnitude; } lastMagnitudes[i] = magnitudes[i]; } } float calcMaxMagnitude(int numBars, float *magnitudes) { float maxMagnitude = 0.0f; for (int i = 0; i < numBars; i++) { if (magnitudes[i] > maxMagnitude) { maxMagnitude = magnitudes[i]; } } if (lastMax < 0.0f) { lastMax = maxMagnitude; return maxMagnitude; } lastMax = (1 - alpha) * lastMax + alpha * maxMagnitude; // Apply exponential smoothing return lastMax; } void clearMagnitudes(int width, float *magnitudes) { for (int i = 0; i < width; i++) { magnitudes[i] = 0.0f; } } void enhancePeaks(int numBars, float *magnitudes, int height) { for (int i = 1; i < numBars - 1; i++) { if (magnitudes[i] > magnitudes[i - 1] && magnitudes[i] > magnitudes[i + 1]) { magnitudes[i] *= 1.3f; // Emphasize the peak magnitudes[i] = fminf(magnitudes[i], (float)height); } } } void calc(int height, int numBars, ma_int32 *audioBuffer, int bitDepth, fftwf_complex *fftInput, fftwf_complex *fftOutput, float *magnitudes, fftwf_plan plan) { int bufferSize = getBufferSize(); if (audioBuffer == NULL) { fprintf(stderr, "Audio buffer is NULL.\n"); return; } for (int i = 0; i < bufferSize; i++) { ma_int32 sample = audioBuffer[i]; float normalizedSample = 0.0f; switch (bitDepth) { case 8: normalizedSample = ((float)sample - 128.0f) / 127.0f; break; case 16: normalizedSample = (float)sample / 32768.0f; break; case 24: { int32_t lower24Bits = sample & 0xFFFFFF; if (lower24Bits & 0x800000) { lower24Bits |= 0xFF000000; // Sign extension } normalizedSample = (float)lower24Bits / 8388608.0f; break; } case 32: // Assuming 32-bit integers normalizedSample = (float)sample / 2147483648.0f; break; default: fprintf(stderr, "Unsupported bit depth: %d\n", bitDepth); return; } fftInput[i][0] = normalizedSample; fftInput[i][1] = 0.0f; } // Apply Windowing (Hamming Window) if (bufferSize > 1) { for (int i = 0; i < bufferSize; i++) { float window = 0.54f - 0.46f * cosf(2.0f * M_PI * i / (bufferSize - 1)); fftInput[i][0] *= window; } } fftwf_execute(plan); // Execute FFT clearMagnitudes(numBars, magnitudes); int halfSize = bufferSize / 2; int limit = (numBars < halfSize) ? numBars : halfSize; for (int i = 0; i < limit; i++) { // Compute magnitude for each frequency bin float real = fftOutput[i][0]; float imag = fftOutput[i][1]; float magnitude = sqrtf(real * real + imag * imag); magnitudes[i] = magnitude; } // Normalize and update magnitudes for visualization float maxMagnitude = calcMaxMagnitude(limit, magnitudes); updateMagnitudes(height, limit, maxMagnitude, magnitudes); enhancePeaks(numBars, magnitudes, height); } wchar_t *upwardMotionChars[] = { L" ", L"▁", L"▂", L"▃", L"▄", L"▅", L"▆", L"▇", L"█"}; wchar_t *getUpwardMotionChar(int level) { if (level < 0 || level > 8) { level = 8; } return upwardMotionChars[level]; } int calcSpectrum(int height, int numBars, fftwf_complex *fftInput, fftwf_complex *fftOutput, float *magnitudes, fftwf_plan plan) { ma_int32 *g_audioBuffer = getAudioBuffer(); ma_format format = getCurrentFormat(); if (format == ma_format_unknown) return -1; int bitDepth = 32; switch (format) { case ma_format_u8: bitDepth = 8; break; case ma_format_s16: bitDepth = 16; break; case ma_format_s24: bitDepth = 24; break; case ma_format_f32: case ma_format_s32: bitDepth = 32; break; default: break; } calc(height, numBars, g_audioBuffer, bitDepth, fftInput, fftOutput, magnitudes, plan); return 0; } PixelData increaseLuminosity(PixelData pixel, int amount) { PixelData pixel2; pixel2.r = pixel.r + amount <= 255 ? pixel.r + amount : 255; pixel2.g = pixel.g + amount <= 255 ? pixel.g + amount : 255; pixel2.b = pixel.b + amount <= 255 ? pixel.b + amount : 255; return pixel2; } void printSpectrum(int height, int width, float *magnitudes, PixelData color, int indentation, bool useProfileColors) { printf("\n"); clearRestOfScreen(); PixelData tmp; for (int j = height; j > 0; j--) { printf("\r"); printBlankSpaces(indentation); if (color.r != 0 || color.g != 0 || color.b != 0) { if (!useProfileColors) { tmp = increaseLuminosity(color, round(j * height * 4)); printf("\033[38;2;%d;%d;%dm", tmp.r, tmp.g, tmp.b); } } else { setDefaultTextColor(); } if (isPaused() || isStopped()) { for (int i = 0; i < width; i++) { printf(" "); } } else { for (int i = 0; i < width; i++) { if (j >= 0) { if (magnitudes[i] >= j) { if (unicodeSupport) { printf(" %S", getUpwardMotionChar(10)); } else { printf(" █"); } } else if (magnitudes[i] + 1 >= j && unicodeSupport) { int firstDecimalDigit = (int)(fmod(magnitudes[i] * 10, 10)); printf(" %S", getUpwardMotionChar(firstDecimalDigit)); } else { printf(" "); } } } } printf("\n "); } printf("\r"); fflush(stdout); } void freeVisuals() { if (fftInput != NULL) { fftwf_free(fftInput); fftInput = NULL; } if (fftOutput != NULL) { fftwf_free(fftOutput); fftOutput = NULL; } } void drawSpectrumVisualizer(int height, int width, PixelData c, int indentation, bool useProfileColors) { bufferSize = getBufferSize(); PixelData color; color.r = c.r; color.g = c.g; color.b = c.b; int numBars = (width / 2); height = height - 1; if (height <= 0 || width <= 0) { return; } if (bufferSize <= 0) return; if (bufferSize != prevBufferSize) { lastMax = -1.0f; freeVisuals(); fftInput = (fftwf_complex *)fftwf_malloc(sizeof(fftwf_complex) * bufferSize); if (fftInput == NULL) { return; } fftOutput = (fftwf_complex *)fftwf_malloc(sizeof(fftwf_complex) * bufferSize); if (fftOutput == NULL) { fftwf_free(fftInput); fftInput = NULL; return; } prevBufferSize = bufferSize; } fftwf_plan plan = fftwf_plan_dft_1d(bufferSize, fftInput, fftOutput, FFTW_FORWARD, FFTW_ESTIMATE); float magnitudes[numBars]; for (int i = 0; i < numBars; i++) { magnitudes[i] = 0.0f; } calcSpectrum(height, numBars, fftInput, fftOutput, magnitudes, plan); printSpectrum(height, numBars, magnitudes, color, indentation, useProfileColors); fftwf_destroy_plan(plan); } kew-2.8.2/src/visuals.h000066400000000000000000000011241467402032100147340ustar00rootroot00000000000000 #include #include #include #include #include #include #include #include "sound.h" #include "term.h" #include "utils.h" #include #ifndef PIXELDATA_STRUCT #define PIXELDATA_STRUCT typedef struct { unsigned char r; unsigned char g; unsigned char b; } PixelData; #endif void initVisuals(); void freeVisuals(); void drawSpectrumVisualizer(int height, int width, PixelData c, int indentation, bool useProfileColors); PixelData increaseLuminosity(PixelData pixel, int amount);