├── .gitignore ├── LICENSE ├── README.md ├── app ├── css │ ├── Montserrat │ │ ├── Montserrat-Black.ttf │ │ ├── Montserrat-BlackItalic.ttf │ │ ├── Montserrat-Bold.ttf │ │ ├── Montserrat-BoldItalic.ttf │ │ ├── Montserrat-ExtraBold.ttf │ │ ├── Montserrat-ExtraBoldItalic.ttf │ │ ├── Montserrat-ExtraLight.ttf │ │ ├── Montserrat-ExtraLightItalic.ttf │ │ ├── Montserrat-Italic.ttf │ │ ├── Montserrat-Light.ttf │ │ ├── Montserrat-LightItalic.ttf │ │ ├── Montserrat-Medium.ttf │ │ ├── Montserrat-MediumItalic.ttf │ │ ├── Montserrat-Regular.ttf │ │ ├── Montserrat-SemiBold.ttf │ │ ├── Montserrat-SemiBoldItalic.ttf │ │ ├── Montserrat-Thin.ttf │ │ ├── Montserrat-ThinItalic.ttf │ │ └── OFL.txt │ ├── colors.css │ ├── content_left.css │ ├── content_right.css │ ├── list_item.css │ ├── main.css │ ├── player.css │ └── variables.css ├── img │ ├── archive-line.svg │ ├── close_black_24dp.svg │ ├── delete.svg │ ├── download.png │ ├── edit-24px.svg │ ├── feed-icon.svg │ ├── generic_podcast_image.png │ ├── history-icon.svg │ ├── ic_add_black_24px.svg │ ├── ic_check_box_black_24px.svg │ ├── ic_check_box_outline_blank_black_24px.svg │ ├── ic_delete_black_24px.svg │ ├── ic_favorite_black_24px.svg │ ├── ic_favorite_border_black_24px.svg │ ├── ic_forward_30_black_24px-old.svg │ ├── ic_forward_30_black_24px.svg │ ├── ic_history_black_24px.svg │ ├── ic_insert_chart_black_24px.svg │ ├── ic_launcher.png │ ├── ic_library_add_black_24px.svg │ ├── ic_more_horiz_black_24px.svg │ ├── ic_pause_black_24px-old.svg │ ├── ic_pause_black_24px.svg │ ├── ic_play_arrow_black_24px-old.svg │ ├── ic_play_arrow_black_24px.svg │ ├── ic_playlist_add_black_24px.svg │ ├── ic_playlist_play_black_24px.svg │ ├── ic_refresh_black_24px.svg │ ├── ic_remove_circle_black_24px.svg │ ├── ic_replay_30_black_24px-old.svg │ ├── ic_replay_30_black_24px.svg │ ├── ic_search_black_24px.svg │ ├── ic_skip_next_black_24px.svg │ ├── ic_skip_previous_black_24px.svg │ ├── ic_view_list_black_24px-old.svg │ ├── ic_view_list_black_24px.svg │ ├── ic_view_module_black_24px-old.svg │ ├── ic_view_module_black_24px.svg │ ├── icon.svg │ ├── inbox-unarchive-line.svg │ ├── information-sign.svg │ ├── keyboard_backspace-24px.svg │ ├── loop_white_24dp.svg │ ├── love-icon.svg │ ├── nothing_found.svg │ ├── playlist-icon.svg │ ├── podcast_07prct.svg │ ├── settings_black_24dp.svg │ ├── sync_problem_white_24dp.svg │ ├── tilde.icns │ ├── tilde.ico │ ├── tilde.png │ ├── tilde.svg │ ├── tilde1024x1024.png │ ├── tilde128x128.ico │ ├── tilde128x128.png │ ├── tilde1600x1600.png │ ├── tilde16x16.ico │ ├── tilde16x16.png │ ├── tilde256x256.ico │ ├── tilde256x256.png │ ├── tilde3200x3200.png │ ├── tilde32x32.ico │ ├── tilde32x32.png │ ├── tilde48x48.ico │ ├── tilde48x48.png │ ├── tilde512x512.icns │ ├── tilde512x512.png │ ├── tilde600x600.png │ ├── tilde64x64.ico │ ├── tilde64x64.png │ ├── tilde800x800.png │ ├── volume-down.svg │ ├── volume-mute.svg │ ├── volume-off.svg │ ├── volume-up.svg │ └── volume.svg ├── index.html ├── js │ ├── archive_class.js │ ├── controller.js │ ├── dark_mode.js │ ├── episode_class.js │ ├── favorite.js │ ├── feed.js │ ├── feeds_class.js │ ├── helper │ │ ├── helper_entries.js │ │ ├── helper_global.js │ │ └── helper_navigation.js │ ├── icons.js │ ├── lib │ │ ├── jquery-ui.min.js │ │ ├── jquery.animateRotate.js │ │ ├── jquery.hoverIntent.min.js │ │ └── jsdom.js │ ├── list_class.js │ ├── list_item.js │ ├── menu.js │ ├── new_episodes_class.js │ ├── pages.js │ ├── playback_class.js │ ├── player_class.js │ ├── podcast_class.js │ ├── request.js │ ├── search.js │ ├── slider_class.js │ ├── translations.js │ ├── ui_class.js │ ├── update_feed_worker.js │ └── xmlparser_worker.js ├── main.js ├── menu.js ├── package-lock.json ├── package.json └── translations │ ├── de.json │ ├── en.json │ ├── fr.json │ ├── i18n.js │ ├── it.json │ ├── pt-BR.json │ └── pt-PT.json └── images ├── logo_github.png ├── logo_github.svg ├── screenshots ├── dark1.png ├── dark2.png ├── dark3.png ├── dark4.png ├── dark5.png ├── dark6.png ├── dark7.png ├── light1.png ├── light2.png ├── light3.png ├── light4.png ├── light5.png ├── light6.png ├── light7.png ├── theme-old.gif └── theme.gif └── tilde.png /.gitignore: -------------------------------------------------------------------------------- 1 | app/node_modules/ 2 | app/dist/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 paologiua 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![logo](images/logo_github.png) 2 | 3 | ### Tilde is the most beautiful and elegant podcast client. 4 | It allows you to search, subscribe and play all your favorite podcasts. 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
Sections
SearchFeedNew Episodes
FavoritesArchiveSettings
Nerdy Things
24 | 25 | # 🔍 Search for new podcasts via iTunes 26 | 27 | The search, based on the iTunes API, allows you to reach any Podcast and view its feed in an instant. 28 | 29 | ![screenshot1](images/screenshots/dark6.png) 30 | 31 | # 🗒️ Viewing the Feeds 32 | 33 | By opening the feed of a podcast, the interface shows all the main informations about it, such as: 34 | 35 | * the name of the podcast 36 | * the name of the podcaster 37 | * the description 38 | 39 | ![screenshot2](images/screenshots/dark7.png) 40 | 41 | After the information section, the list of episodes is shown. 42 | 43 | # 🎙️ New episodes 44 | 45 | The section of new episodes is displayed when the app is launched. It shows the most recent episodes published during the last week. 46 | 47 | ![screenshot3](images/screenshots/dark1.png) 48 | 49 | # ❤️ Favorites 50 | 51 | Episodes from a podcast are shown in the section of new episodes only after you have added it to your favorites. 52 | 53 | The section of favorites allows you to have quick links to all the podcasts you love most. 54 | 55 | ![screenshot4](images/screenshots/dark2.png) 56 | 57 | # 📥 Archive 58 | 59 | You can keep the most interesting episodes in your personal archive. 60 | 61 | ![screenshot5](images/screenshots/dark3.png) 62 | 63 | # ⚙️ Settings 64 | 65 | In the settings you can choose the theme you prefer. 66 | 67 | ![screenshot6](images/screenshots/theme.gif) 68 | 69 | # 👾 Nerdy Things 70 | 71 | **🚧 Work in progress 🚧** 72 | 73 | ### This project is a fork of [Poddycast](https://github.com/MrChuckomo/poddycast) 74 | -------------------------------------------------------------------------------- /app/css/Montserrat/Montserrat-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/css/Montserrat/Montserrat-Black.ttf -------------------------------------------------------------------------------- /app/css/Montserrat/Montserrat-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/css/Montserrat/Montserrat-BlackItalic.ttf -------------------------------------------------------------------------------- /app/css/Montserrat/Montserrat-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/css/Montserrat/Montserrat-Bold.ttf -------------------------------------------------------------------------------- /app/css/Montserrat/Montserrat-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/css/Montserrat/Montserrat-BoldItalic.ttf -------------------------------------------------------------------------------- /app/css/Montserrat/Montserrat-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/css/Montserrat/Montserrat-ExtraBold.ttf -------------------------------------------------------------------------------- /app/css/Montserrat/Montserrat-ExtraBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/css/Montserrat/Montserrat-ExtraBoldItalic.ttf -------------------------------------------------------------------------------- /app/css/Montserrat/Montserrat-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/css/Montserrat/Montserrat-ExtraLight.ttf -------------------------------------------------------------------------------- /app/css/Montserrat/Montserrat-ExtraLightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/css/Montserrat/Montserrat-ExtraLightItalic.ttf -------------------------------------------------------------------------------- /app/css/Montserrat/Montserrat-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/css/Montserrat/Montserrat-Italic.ttf -------------------------------------------------------------------------------- /app/css/Montserrat/Montserrat-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/css/Montserrat/Montserrat-Light.ttf -------------------------------------------------------------------------------- /app/css/Montserrat/Montserrat-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/css/Montserrat/Montserrat-LightItalic.ttf -------------------------------------------------------------------------------- /app/css/Montserrat/Montserrat-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/css/Montserrat/Montserrat-Medium.ttf -------------------------------------------------------------------------------- /app/css/Montserrat/Montserrat-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/css/Montserrat/Montserrat-MediumItalic.ttf -------------------------------------------------------------------------------- /app/css/Montserrat/Montserrat-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/css/Montserrat/Montserrat-Regular.ttf -------------------------------------------------------------------------------- /app/css/Montserrat/Montserrat-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/css/Montserrat/Montserrat-SemiBold.ttf -------------------------------------------------------------------------------- /app/css/Montserrat/Montserrat-SemiBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/css/Montserrat/Montserrat-SemiBoldItalic.ttf -------------------------------------------------------------------------------- /app/css/Montserrat/Montserrat-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/css/Montserrat/Montserrat-Thin.ttf -------------------------------------------------------------------------------- /app/css/Montserrat/Montserrat-ThinItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/css/Montserrat/Montserrat-ThinItalic.ttf -------------------------------------------------------------------------------- /app/css/Montserrat/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2011 The Montserrat Project Authors (https://github.com/JulietaUla/Montserrat) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /app/css/colors.css: -------------------------------------------------------------------------------- 1 | body { 2 | --main-color: 68, 138, 255; /* #448aff */ 3 | --main-color-hover: 41, 121, 255; /* #2979ff */ 4 | 5 | --text-color: 0, 0, 0; /* #000 */ 6 | --opaque-text-color: var(--text-color), 0.6; 7 | 8 | --layer-0-color: 255, 255, 255; /* #fff */ 9 | --layer-1-color: 238, 238, 238; /* #eee */ 10 | --layer-2-color: 221, 221, 221; /* #ddd */ 11 | 12 | --btn-1-color: 221, 221, 221; /* #ddd */ 13 | --btn-1-color-hover: 204, 204, 204; /* #ccc; */ 14 | --btn-1-color-active: 187, 187, 187; /* #bbb */ 15 | 16 | --window-color: 221, 221, 221; /* #ddd */ 17 | 18 | --right-list-el-color: 233, 233, 233; /* #e9e9e9 */ 19 | --right-list-el-color-hover: 221, 221, 221; /* #ddd */ 20 | --right-list-el-color-info: 215, 215, 215; /* #d7d7d7 */ 21 | 22 | --slider-color: 211, 211, 211; /* #d3d3d3 */ 23 | 24 | --volume-btn-color-1: var(--layer-1-color); 25 | --volume-btn-color-2: var(--layer-0-color); 26 | 27 | --flag-color-1: 153, 153, 153; /* #999 */ 28 | --flag-color-2: 187, 187, 187; /* #bbb */ 29 | 30 | --progress-download-color: 215, 215, 215; /* #d7d7d7 */ 31 | --progress-download-color-hover: 209, 209, 209; /* #d1d1d1 */ 32 | 33 | --link-color: 54, 79, 122; /* #364f7a */ 34 | 35 | --shadow-color: 100, 100, 100; /* #646464 */ 36 | } 37 | 38 | .dark-mode { 39 | --text-color: 255, 255, 255; /*#fff*/ 40 | 41 | --layer-0-color: 51, 51, 51; /* #333 */ 42 | --layer-1-color: 34, 34, 34; /* #222 */ 43 | --layer-2-color: 23, 23, 23; /*#171717 */ 44 | 45 | --btn-1-color: 23, 23, 23; /*#171717 */ 46 | --btn-1-color-hover: 17, 17, 17; /* #111 */ 47 | --btn-1-color-active: 0, 0, 0; /* #000 */ 48 | 49 | --window-color: 51, 51, 51; /* #333 */ 50 | 51 | --right-list-el-color: 39, 39, 39; /* #272727 */ 52 | --right-list-el-color-hover: 34, 34, 34; /* #222 */ 53 | --right-list-el-color-info: 24, 24, 24; /* #181818 */ 54 | 55 | --slider-color: 34, 34, 34; /* #222 */ 56 | 57 | --flag-color-1: 68, 68, 68; /* #444 */ 58 | --flag-color-2: 25, 25, 25; /* #191919 */ 59 | 60 | --progress-download-color: 32, 32, 32; /* #202020 */ 61 | --progress-download-color-hover: 25, 25, 25; /* #191919 */ 62 | 63 | --link-color: 88, 128, 199; /* #5880c7 */ 64 | 65 | --shadow-color: 0, 0, 0; /* #000 */ 66 | } 67 | -------------------------------------------------------------------------------- /app/css/content_left.css: -------------------------------------------------------------------------------- 1 | 2 | #content-left { 3 | height: 100%; 4 | width: 300px; 5 | display: grid; 6 | position: fixed; 7 | overflow: hidden; 8 | background-color: rgb(var(--layer-1-color)); 9 | grid-template-rows: 70px 1fr auto; 10 | grid-template-columns: 100%; 11 | grid-template-areas: 12 | "search" 13 | "menu" 14 | "player"; 15 | } 16 | 17 | #bar-search { 18 | background-color: rgb(var(--btn-1-color)); 19 | margin: 0.8em; 20 | margin-bottom: 2em; 21 | margin-top: 1em; 22 | height: 42px; 23 | border-radius: 10px; 24 | display: grid; 25 | grid-template-columns: auto 1fr; 26 | grid-template-rows: 40px; 27 | grid-template-areas: 28 | 'reload' 29 | 'search'; 30 | box-shadow: 0px 2px 0px 0px rgba(0, 0, 0, 0.1); 31 | } 32 | 33 | #bar-search > svg { 34 | margin: 11.5px; 35 | margin-right: 10px; 36 | grid-area: reload; 37 | opacity: 0.6; 38 | transition: width .1s linear, margin .1s linear, opacity .1s linear; 39 | } 40 | 41 | #bar-search > svg:hover { 42 | opacity: 1; 43 | } 44 | 45 | #search { 46 | display: flex; 47 | height: 40px; 48 | padding-left: 0.5em; 49 | background: rgb(var(--layer-0-color)); 50 | border-radius: 10px; 51 | border: 1px solid rgb(var(--layer-1-color)); 52 | box-shadow: 0px 2px 0px 0px rgba(0, 0, 0, 0.1); 53 | } 54 | 55 | #search svg { 56 | opacity: 0.6; 57 | height: 100%; 58 | vertical-align: middle; 59 | } 60 | 61 | #search input { 62 | font-size: 16px; 63 | padding: 0.5em; 64 | margin-right: 0.5em; 65 | width: calc(100% - 1.2em); 66 | border: 0; 67 | background-color: inherit; 68 | outline: none; 69 | } 70 | 71 | .search-animation > svg { 72 | opacity: 0 !important; 73 | width: 0px; 74 | margin: 0px !important; 75 | margin-top: 10px !important; 76 | } 77 | 78 | #menu { 79 | grid-area: menu; 80 | } 81 | 82 | #menu ul { 83 | padding: 0; 84 | margin-top: 0; 85 | } 86 | 87 | #menu ul li { 88 | list-style: none; 89 | padding-bottom: 0.5em; 90 | padding-top: 0.5em; 91 | padding-left: 1.2em; 92 | padding-right: 0.8em; 93 | font-size: 16px; 94 | display: grid; 95 | grid-template-rows: 100%; 96 | grid-template-columns: 30px 1fr 30px; 97 | 98 | border-radius: 10px; 99 | margin: 4px 8px; 100 | } 101 | 102 | 103 | #menu ul li span { 104 | align-self: center; 105 | } 106 | 107 | #menu ul li svg { 108 | opacity: 0.8; 109 | align-self: center; 110 | } 111 | 112 | .menu-count { 113 | align-self: center; 114 | color: rgba(var(--opaque-text-color)); 115 | } 116 | 117 | .selected { 118 | background-color: rgb(var(--btn-1-color)); 119 | } 120 | 121 | .pink { 122 | fill: #E91E63; 123 | } 124 | 125 | .orange { 126 | fill: #ff8d0c; 127 | } 128 | 129 | .dark-mode .orange { 130 | fill: #ffc247; 131 | } 132 | 133 | .blue { 134 | fill: #039BE5; 135 | } 136 | 137 | .purple { 138 | fill: #9C27B0; 139 | } 140 | 141 | .teal { 142 | fill: #009688; 143 | } 144 | 145 | @keyframes rotation { 146 | from { 147 | transform: rotate(0deg); 148 | } to { 149 | transform: rotate(-359deg); 150 | } 151 | } 152 | 153 | .is-refreshing { 154 | animation: rotation 1.5s infinite linear; 155 | } 156 | -------------------------------------------------------------------------------- /app/css/list_item.css: -------------------------------------------------------------------------------- 1 | .list-item-row-layout { 2 | display: grid; 3 | grid-template-rows: 1fr; 4 | font-size: 14px; 5 | height: 3.2em; 6 | padding: 0.5em; 7 | padding-left: 2em; 8 | 9 | border-radius: 15px; 10 | margin: 10px; 11 | background-color: rgb(var(--right-list-el-color)); 12 | 13 | transition: background-color .1s linear; 14 | } 15 | 16 | .list-item-row-layout:hover { 17 | background-color: rgb(var(--right-list-el-color-hover)); 18 | } 19 | 20 | .list-item-row-layout img { 21 | border-radius: 5px; 22 | width: 40px; 23 | height: 40px; 24 | margin: 2.396px 0; 25 | box-shadow: var(--box-shadow); 26 | } 27 | 28 | .list-item-icon { 29 | opacity: 0.6; 30 | align-self: center; 31 | text-align: center; 32 | height: 100%; 33 | } 34 | 35 | .list-item-icon svg { 36 | position: relative; 37 | top: 50%; 38 | -ms-transform: translateY(-50%); 39 | transform: translateY(-50%); 40 | } 41 | 42 | .list-item-icon:hover { 43 | opacity: 0.8; 44 | } 45 | 46 | .list-item-flag { 47 | border-radius: 4px; 48 | text-align: center; 49 | width: 80%; 50 | padding: 0.2em; 51 | align-self: center; 52 | text-overflow: ellipsis; 53 | white-space: nowrap; 54 | overflow: hidden; 55 | 56 | --percentage: 0%; 57 | 58 | background: linear-gradient(to right, rgb(var(--flag-color-1)) 0%, 59 | rgb(var(--flag-color-1)) 50%, 60 | rgb(var(--flag-color-2)) 50%, 61 | rgb(var(--flag-color-2)) 100%), 62 | rgb(var(--flag-color-2)); 63 | 64 | background-size: calc(var(--percentage)*2); 65 | background-repeat: no-repeat; 66 | transition: background-size .2s linear; 67 | 68 | font-weight: bold; 69 | padding-top: 7px; 70 | padding-bottom: 7px; 71 | margin-left: 5px; 72 | } 73 | 74 | .list-item-text, 75 | .list-item-bold-text, 76 | .list-item-description, 77 | .list-item-sub-text { 78 | text-overflow: ellipsis; 79 | white-space: nowrap; 80 | overflow: hidden; 81 | align-self: center; 82 | padding-right: 0.5em; 83 | padding-left: 0.5em; 84 | } 85 | 86 | .list-item-text { 87 | max-width: fit-content; 88 | } 89 | 90 | .list-item-bold-text { 91 | font-weight: bold; 92 | } 93 | 94 | .list-item-sub-text { 95 | color: rgba(var(--opaque-text-color)); 96 | } 97 | 98 | .download-in-progress { 99 | --bk-color1-download: rgb(var(--progress-download-color)); 100 | --bk-color2-download: rgb(var(--right-list-el-color)); 101 | --progress: 0%; 102 | background: linear-gradient(to right, var(--bk-color1-download) 0%, 103 | var(--bk-color1-download) 50%, 104 | var(--bk-color2-download) 50%, 105 | var(--bk-color2-download) 100%), 106 | var(--bk-color2-download); 107 | background-size: calc(var(--progress)*2); 108 | background-repeat: no-repeat; 109 | transition: background-size .2s linear; 110 | } 111 | 112 | .download-in-progress:hover { 113 | --bk-color1-download: rgb(var(--progress-download-color-hover)); 114 | --bk-color2-download: rgb(var(--right-list-el-color-hover)); 115 | 116 | background: linear-gradient(to right, var(--bk-color1-download) 0%, 117 | var(--bk-color1-download) 50%, 118 | var(--bk-color2-download) 50%, 119 | var(--bk-color2-download) 100%), 120 | var(--bk-color2-download); 121 | 122 | background-size: calc(var(--progress)*2); 123 | background-repeat: no-repeat; 124 | } 125 | 126 | #list li[info-mode] { 127 | background: rgb(var(--right-list-el-color-info)); 128 | } 129 | 130 | #list li[info-mode] .list-item-description { 131 | height: 40px; 132 | align-self: unset; 133 | position: relative; 134 | top: 22px; 135 | margin: 2.396px 0; 136 | } 137 | 138 | .list-item-text:hover { 139 | text-decoration: underline; 140 | padding-top: 10px; 141 | padding-bottom: 10px; 142 | } 143 | 144 | @keyframes pulsation { 145 | 0% { 146 | opacity: 1; 147 | } 148 | 50% { 149 | opacity: .6; 150 | } 151 | 100% { 152 | opacity: 1; 153 | } 154 | } 155 | 156 | .download-in-progress-icon { 157 | transform-origin: center; 158 | transform-box: fill-box; 159 | animation: rotation 1.5s infinite linear; 160 | } 161 | 162 | svg .hover-icon { 163 | display: none; 164 | } 165 | 166 | svg:hover .default-icon { 167 | display: none; 168 | } 169 | 170 | svg:hover .hover-icon { 171 | display: inline-block; 172 | } -------------------------------------------------------------------------------- /app/css/main.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Roboto'); 2 | 3 | @font-face { 4 | font-family: "Montserrat"; 5 | src: url(Montserrat/Montserrat-Regular.ttf) format("truetype"); 6 | } 7 | 8 | ::-webkit-scrollbar { 9 | background-color: #ddd; 10 | width: .8em 11 | } 12 | 13 | .dark-mode ::-webkit-scrollbar { 14 | background-color: #222; 15 | } 16 | 17 | ::-webkit-scrollbar-thumb:hover { 18 | background-color: #999; 19 | } 20 | 21 | .dark-mode ::-webkit-scrollbar-thumb:hover { 22 | background-color: #555; 23 | } 24 | 25 | ::-webkit-scrollbar-thumb:active { 26 | background-color: #888; 27 | } 28 | 29 | .dark-mode ::-webkit-scrollbar-thumb:active { 30 | background-color: #666; 31 | } 32 | 33 | ::-webkit-scrollbar-thumb:window-inactive, 34 | ::-webkit-scrollbar-thumb { 35 | background: #aaa; 36 | } 37 | 38 | .dark-mode ::-webkit-scrollbar-thumb:window-inactive, 39 | .dark-mode ::-webkit-scrollbar-thumb { 40 | background: #444; 41 | } 42 | 43 | ::selection { 44 | background-color: #aaa; 45 | color: #eee; 46 | } 47 | 48 | img { 49 | user-drag: none; 50 | user-select: none; 51 | -moz-user-select: none; 52 | -webkit-user-drag: none; 53 | -webkit-user-select: none; 54 | -ms-user-select: none; 55 | } 56 | 57 | input { 58 | font-family: "Montserrat", "Roboto", sans-serif; 59 | color: rgb(var(--text-color)); 60 | } 61 | 62 | input::placeholder { 63 | transition: color .2s linear; 64 | } 65 | 66 | input:hover::placeholder { 67 | color: rgb(var(--text-color)); 68 | } 69 | 70 | button { 71 | color: rgb(var(--text-color)); 72 | } 73 | 74 | svg { 75 | fill: rgb(var(--text-color)); 76 | } 77 | 78 | h1 { 79 | margin: 0; 80 | padding: 0.5em; 81 | display: inline-block; 82 | width: calc(100% - 2em); 83 | text-overflow: ellipsis; 84 | white-space: nowrap; 85 | overflow: hidden; 86 | } 87 | 88 | a { 89 | -ms-word-break: break-all; 90 | word-break: break-all; 91 | /* Non standard for webkit */ 92 | word-break: break-word; 93 | 94 | -webkit-hyphens: auto; 95 | -moz-hyphens: auto; 96 | -ms-hyphens: auto; 97 | hyphens: auto; 98 | } 99 | 100 | a:link, 101 | a:visited { 102 | color: rgb(var(--link-color)); 103 | background-color: transparent; 104 | text-decoration: none; 105 | } 106 | 107 | a:hover, 108 | a:active { 109 | color: rgb(var(--link-color)); 110 | background-color: transparent; 111 | text-decoration: underline; 112 | } 113 | 114 | body { 115 | margin: 0; 116 | padding: 0; 117 | font-family: "Montserrat", "Roboto", sans-serif; 118 | user-select: none; 119 | cursor: default; 120 | color: rgb(var(--text-color)); 121 | } 122 | 123 | .switch { 124 | --line: #e8ebfb; 125 | --dot: rgb(var(--main-color)); 126 | --circle: #d3d4ec; 127 | --background: #fff; 128 | --duration: 0.3s; 129 | --text: #9ea0be; 130 | --shadow: 0 1px 3px rgba(0, 9, 61, .08); 131 | cursor: pointer; 132 | position: relative; 133 | } 134 | 135 | .switch:before { 136 | content: ''; 137 | width: 60px; 138 | height: 32px; 139 | border-radius: 16px; 140 | background: var(--background); 141 | position: absolute; 142 | left: 0; 143 | top: 0; 144 | box-shadow: var(--shadow); 145 | } 146 | 147 | .switch input { 148 | display: none; 149 | } 150 | 151 | .switch input + div:before, .switch input + div:after { 152 | --s: 1; 153 | content: ''; 154 | position: absolute; 155 | height: 4px; 156 | top: 14px; 157 | width: 24px; 158 | background: var(--line); 159 | transform: scaleX(var(--s)); 160 | transition: transform var(--duration) ease; 161 | } 162 | 163 | .switch input + div:before { 164 | --s: 0; 165 | left: 4px; 166 | transform-origin: 0 50%; 167 | border-radius: 2px 0 0 2px; 168 | } 169 | 170 | .switch input + div:after { 171 | left: 32px; 172 | transform-origin: 100% 50%; 173 | border-radius: 0 2px 2px 0; 174 | } 175 | 176 | .switch input + div span { 177 | padding-left: 60px; 178 | line-height: 28px; 179 | color: var(--text); 180 | } 181 | 182 | .switch input + div span:before { 183 | --x: 0; 184 | --b: var(--circle); 185 | --s: 4px; 186 | content: ''; 187 | position: absolute; 188 | left: 4px; 189 | top: 4px; 190 | width: 24px; 191 | height: 24px; 192 | border-radius: 50%; 193 | box-shadow: inset 0 0 0 var(--s) var(--b); 194 | transform: translateX(var(--x)); 195 | transition: box-shadow var(--duration) ease, transform var(--duration) ease; 196 | } 197 | 198 | .switch input + div span:not(:empty) { 199 | padding-left: 68px; 200 | } 201 | 202 | .switch input:checked + div:before { 203 | --s: 1; 204 | } 205 | 206 | .switch input:checked + div:after { 207 | --s: 0; 208 | } 209 | 210 | .switch input:checked + div span:before { 211 | --x: 28px; 212 | --s: 12px; 213 | --b: var(--dot); 214 | } 215 | 216 | .settings-ui { 217 | display: none; 218 | } 219 | 220 | .settings-ui-bk { 221 | position: fixed; 222 | height: 100vh; 223 | width: 100vw; 224 | background-color: #00000070; 225 | z-index: 1001; 226 | } 227 | 228 | .settings-ui-window { 229 | position: fixed; 230 | height: min-content; 231 | width: 390px; 232 | border-radius: 20px; 233 | background-color: rgb(var(--window-color)); 234 | margin: calc(50vh - 100px) calc(50vw - 225px); 235 | box-shadow: var(--box-shadow); 236 | z-index: 1002; 237 | padding: 30px; 238 | } 239 | 240 | .settings-ui-title { 241 | display: block; 242 | font-size: 2em; 243 | font-weight: bold; 244 | } 245 | 246 | .settings-ui-title > svg { 247 | float: right; 248 | opacity: .7; 249 | transition: opacity .2s linear; 250 | } 251 | 252 | .settings-ui-title > svg:hover { 253 | opacity: 1; 254 | } 255 | 256 | .settings-ui-row { 257 | font-size: 20px; 258 | margin: 35px 0 10px 0; 259 | display: block; 260 | line-height: 32px; 261 | } 262 | 263 | .settings-ui-row > label { 264 | float: right; 265 | } -------------------------------------------------------------------------------- /app/css/player.css: -------------------------------------------------------------------------------- 1 | /* ------------------------------------------------------------------------------------------------------------------ */ 2 | /* NOTE: Player */ 3 | /* ------------------------------------------------------------------------------------------------------------------ */ 4 | 5 | #replay-30-sec { 6 | grid-area: replay; 7 | align-self: center; 8 | justify-self: center; 9 | } 10 | 11 | #play-pause { 12 | grid-area: play; 13 | align-self: center; 14 | justify-self: center; 15 | } 16 | 17 | #forward-30-sec { 18 | grid-area: forward; 19 | align-self: center; 20 | justify-self: center; 21 | } 22 | 23 | #slider-container { 24 | padding-left: 5px; 25 | padding-right: 5px; 26 | position: relative; 27 | bottom: 3px; 28 | } 29 | 30 | .slider { 31 | -webkit-appearance: none; 32 | width: 100%; 33 | height: 8px; 34 | border-radius: 5px; 35 | --progress-slider: 0%; 36 | background: linear-gradient(to right, rgb(var(--main-color)) 0%, 37 | rgb(var(--main-color)) var(--progress-slider), 38 | rgb(var(--slider-color)) var(--progress-slider), 39 | rgb(var(--slider-color)) 100%); 40 | outline: none; 41 | opacity: 0.7; 42 | -webkit-transition: .2s; 43 | transition: opacity .2s; 44 | } 45 | 46 | .slider:hover { 47 | opacity: 1; 48 | } 49 | 50 | .slider::-webkit-slider-thumb { 51 | -webkit-appearance: none; 52 | appearance: none; 53 | width: 0px; 54 | height: 0px; 55 | border-radius: 50%; 56 | background: rgb(var(--main-color)); 57 | cursor: pointer; 58 | 59 | transition: width .2s, height .2s; 60 | } 61 | 62 | .slider:hover::-webkit-slider-thumb { 63 | width: 12px; 64 | height: 12px; 65 | 66 | transition: width .2s, height .2s; 67 | } 68 | 69 | #content-left-player { 70 | padding: 10px; 71 | margin: 0 0.8em 0.8em 0.8em; 72 | border-radius: 15px; 73 | background-color: rgb(var(--layer-0-color)); 74 | height: min-content; 75 | grid-area: player; 76 | display: grid; 77 | grid-template-rows: 120px 35px 60px 20px 20px 60px; 78 | grid-template-columns: 100%; 79 | box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.1); 80 | } 81 | 82 | #content-left-player-img { 83 | width: 100%; 84 | display: flex; 85 | margin: 10px 0; 86 | } 87 | 88 | #content-left-player-img>img { 89 | margin: auto; 90 | border-radius: 10px; 91 | height: 100px; 92 | width: auto; 93 | background-color: rgb(var(--layer-1-color)); 94 | box-shadow: var(--box-shadow); 95 | } 96 | 97 | #content-left-player-title { 98 | width: 100%; 99 | display: flex; 100 | } 101 | 102 | #content-left-player-title>div { 103 | margin: auto; 104 | padding: 10px; 105 | 106 | text-overflow: ellipsis; 107 | white-space: nowrap; 108 | overflow: hidden; 109 | max-width: 240px; 110 | } 111 | 112 | #content-left-player-buttons { 113 | width: 100%; 114 | display: flex; 115 | } 116 | 117 | #content-left-player-buttons>div { 118 | margin: auto; 119 | } 120 | 121 | #content-left-player-buttons>div>svg { 122 | width: 40px; 123 | height: auto; 124 | opacity: 0.65; 125 | transition: opacity .2s ease-out; 126 | } 127 | 128 | #content-left-player-buttons>div>svg:hover { 129 | opacity: 1; 130 | } 131 | 132 | #content-left-player-time { 133 | float: left; 134 | font-size: small; 135 | } 136 | 137 | #content-left-player-duration { 138 | float: right; 139 | text-align: right; 140 | font-size: small; 141 | } 142 | 143 | .content-left-player-btn { 144 | display: flex; 145 | grid-area: speed; 146 | align-self: center; 147 | justify-self: center; 148 | outline: 0; 149 | font-size: 12px; 150 | height: 40px; 151 | width: 80px; 152 | border: .1em solid rgb(var(--layer-1-color)); 153 | border-radius: 10px; 154 | background-color: rgb(var(--layer-1-color)); 155 | } 156 | 157 | .content-left-player-btn button { 158 | opacity: 0.6; 159 | transition: opacity .2s linear; 160 | } 161 | 162 | .content-left-player-btn button, 163 | .content-left-player-btn:hover button, 164 | .content-left-player-btn:active button { 165 | outline: 0; 166 | border: 0; 167 | background-color: inherit; 168 | } 169 | 170 | .content-left-player-btn:hover button, 171 | .content-left-player-btn:active button { 172 | opacity: 1; 173 | } 174 | 175 | .content-left-player-btn:hover { 176 | border-color: rgb(var(--btn-1-color-hover)); 177 | } 178 | 179 | .content-left-player-btn:active { 180 | border-color: rgb(var(--btn-1-color-active)); 181 | } 182 | 183 | button#content-left-player-volume-indicator, 184 | button#content-left-player-speed-indicator { 185 | width: 3.3em; 186 | padding: 0; 187 | font-weight: bold; 188 | } 189 | 190 | .content-left-player-btn { 191 | margin: 10px; 192 | overflow: hidden; 193 | } 194 | 195 | #content-left-player-volume-btn { 196 | float: left; 197 | margin-left: 30px; 198 | --volume: 100%; 199 | } 200 | 201 | #content-left-player-volume-indicator svg { 202 | height: 23px; 203 | } 204 | 205 | #content-left-player-volume-indicator svg path { 206 | fill: rgb(var(--text-color)); 207 | } 208 | 209 | #content-left-player-speed-btn { 210 | float: right; 211 | margin-right: 30px; 212 | } 213 | 214 | .content-left-player-btn>button>span { 215 | font-size: 20px; 216 | } 217 | 218 | #content-left-player-volume-btn:hover { 219 | background: linear-gradient(to right, rgb(var(--volume-btn-color-1)) 0%, 220 | rgb(var(--volume-btn-color-1)) var(--volume), 221 | rgb(var(--volume-btn-color-2)) var(--volume), 222 | rgb(var(--volume-btn-color-2)) 100%); 223 | } -------------------------------------------------------------------------------- /app/css/variables.css: -------------------------------------------------------------------------------- 1 | body { 2 | --titlebar-height: 35px; 3 | 4 | --box-shadow: 0px 2px 5px 0px rgba(var(--shadow-color), 0.6); 5 | } -------------------------------------------------------------------------------- /app/img/archive-line.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/img/close_black_24dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/img/delete.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/img/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/img/download.png -------------------------------------------------------------------------------- /app/img/edit-24px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/img/feed-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 36 | 44 | 48 | 52 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /app/img/generic_podcast_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/img/generic_podcast_image.png -------------------------------------------------------------------------------- /app/img/history-icon.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 34 | 37 | 40 | 43 | 46 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /app/img/ic_add_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/img/ic_check_box_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/img/ic_check_box_outline_blank_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/img/ic_delete_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/img/ic_favorite_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/img/ic_favorite_border_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/img/ic_forward_30_black_24px-old.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/img/ic_forward_30_black_24px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/img/ic_history_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/img/ic_insert_chart_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/img/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/img/ic_launcher.png -------------------------------------------------------------------------------- /app/img/ic_library_add_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/img/ic_more_horiz_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/img/ic_pause_black_24px-old.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/img/ic_pause_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/img/ic_play_arrow_black_24px-old.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/img/ic_play_arrow_black_24px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/img/ic_playlist_add_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/img/ic_playlist_play_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/img/ic_refresh_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/img/ic_remove_circle_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/img/ic_replay_30_black_24px-old.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/img/ic_replay_30_black_24px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/img/ic_search_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/img/ic_skip_next_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/img/ic_skip_previous_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/img/ic_view_list_black_24px-old.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/img/ic_view_list_black_24px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/img/ic_view_module_black_24px-old.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/img/ic_view_module_black_24px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/img/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/img/inbox-unarchive-line.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/img/information-sign.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/img/keyboard_backspace-24px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/img/loop_white_24dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/img/love-icon.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | 24 | 27 | 30 | 31 | 34 | 37 | 40 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/img/nothing_found.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/img/playlist-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/img/podcast_07prct.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/img/settings_black_24dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/img/sync_problem_white_24dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/img/tilde.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/img/tilde.icns -------------------------------------------------------------------------------- /app/img/tilde.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/img/tilde.ico -------------------------------------------------------------------------------- /app/img/tilde.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/img/tilde.png -------------------------------------------------------------------------------- /app/img/tilde1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/img/tilde1024x1024.png -------------------------------------------------------------------------------- /app/img/tilde128x128.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/img/tilde128x128.ico -------------------------------------------------------------------------------- /app/img/tilde128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/img/tilde128x128.png -------------------------------------------------------------------------------- /app/img/tilde1600x1600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/img/tilde1600x1600.png -------------------------------------------------------------------------------- /app/img/tilde16x16.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/img/tilde16x16.ico -------------------------------------------------------------------------------- /app/img/tilde16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/img/tilde16x16.png -------------------------------------------------------------------------------- /app/img/tilde256x256.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/img/tilde256x256.ico -------------------------------------------------------------------------------- /app/img/tilde256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/img/tilde256x256.png -------------------------------------------------------------------------------- /app/img/tilde3200x3200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/img/tilde3200x3200.png -------------------------------------------------------------------------------- /app/img/tilde32x32.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/img/tilde32x32.ico -------------------------------------------------------------------------------- /app/img/tilde32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/img/tilde32x32.png -------------------------------------------------------------------------------- /app/img/tilde48x48.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/img/tilde48x48.ico -------------------------------------------------------------------------------- /app/img/tilde48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/img/tilde48x48.png -------------------------------------------------------------------------------- /app/img/tilde512x512.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/img/tilde512x512.icns -------------------------------------------------------------------------------- /app/img/tilde512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/img/tilde512x512.png -------------------------------------------------------------------------------- /app/img/tilde600x600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/img/tilde600x600.png -------------------------------------------------------------------------------- /app/img/tilde64x64.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/img/tilde64x64.ico -------------------------------------------------------------------------------- /app/img/tilde64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/img/tilde64x64.png -------------------------------------------------------------------------------- /app/img/tilde800x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/app/img/tilde800x800.png -------------------------------------------------------------------------------- /app/img/volume-down.svg: -------------------------------------------------------------------------------- 1 | 10 | 12 | 13 | 19 | 20 | -------------------------------------------------------------------------------- /app/img/volume-mute.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/img/volume-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/img/volume-up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/img/volume.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/js/controller.js: -------------------------------------------------------------------------------- 1 | function initController() { 2 | initLink(); 3 | initInput(); 4 | 5 | enableOpenLinkInDefaultBrowser(); 6 | 7 | $('body').on('click', '.list-item-text', (e) => { 8 | showAllEpisodes($(e.target).parent().get(0)) 9 | }); 10 | $('body').on('click', '.info-channel', (e) => { 11 | showAllEpisodes($(e.target).parent().parent().get(0)) 12 | }); 13 | } 14 | 15 | function enableOpenLinkInDefaultBrowser() { 16 | $('body').on('click', 'a', (event) => { 17 | event.preventDefault(); 18 | require("electron").shell.openExternal(event.target.href); 19 | }); 20 | 21 | $('body').on('auxclick', 'a', (event) => { 22 | event.preventDefault(); 23 | require("electron").shell.openExternal(event.target.href); 24 | }); 25 | } 26 | 27 | /* 28 | * Link 29 | */ 30 | 31 | function initLink() { 32 | $('#menu-episodes').click(() => showPage('newEpisodes')) 33 | $('#menu-favorites').click(() => showPage('favorites')) 34 | $('#menu-refresh').click(readFeeds) 35 | $('#menu-settings').click(() => showPage('settings')) 36 | $('#menu-archive').click(() => showPage('archive')) 37 | $('#menu-statistics').click(() => showPage('statistics')) 38 | } 39 | 40 | /* 41 | * Input 42 | */ 43 | /* 44 | function matchText(e) { 45 | var char = String.fromCharCode(e.which) 46 | if (char.match(/^[^A-Za-z0-9+!?#\.\-]+$/)) 47 | e.preventDefault(); 48 | } 49 | 50 | function matchTextSearch(e) { 51 | var char = String.fromCharCode(e.which) 52 | if (char.match(/^[^A-Za-z0-9+!?#\.\-\ ']+$/)) 53 | e.preventDefault(); 54 | } 55 | */ 56 | function initInput() { 57 | /* $('input').not('#search-input').keypress(function (e) { 58 | matchText(e) 59 | }) 60 | 61 | $('#search-input').keypress(function (e) { 62 | matchTextSearch(e) 63 | }) */ 64 | 65 | $('#search-input').keyup(function (e) { 66 | if(this.value == '') 67 | $("#bar-search").removeClass("search-animation"); 68 | else 69 | $("#bar-search").addClass("search-animation"); 70 | search(this, e); 71 | }) 72 | .focusout(function (e) { 73 | setTimeout(() => { 74 | $("#bar-search").removeClass("search-animation"); 75 | }, 500); 76 | }) 77 | .focusin(function (e) { 78 | if(this.value != '') 79 | $("#bar-search").addClass("search-animation"); 80 | }) 81 | 82 | } 83 | 84 | function _(obj) { 85 | return {...$(obj).data()}; 86 | } -------------------------------------------------------------------------------- /app/js/dark_mode.js: -------------------------------------------------------------------------------- 1 | const {Menu} = require('electron').remote 2 | 3 | // --------------------------------------------------------------------------------------------------------------------- 4 | 5 | function setUIDark() { 6 | $('body').addClass('dark-mode'); 7 | 8 | if(titlebar != null) { 9 | const customTitlebar = require('custom-electron-titlebar'); 10 | titlebar.updateBackground(customTitlebar.Color.fromHex('#1c1c1c')); 11 | } 12 | } 13 | 14 | function setUILight() { 15 | $('body').removeClass('dark-mode'); 16 | 17 | if(titlebar != null) { 18 | const customTitlebar = require('custom-electron-titlebar'); 19 | titlebar.updateBackground(customTitlebar.Color.fromHex('#bbb')); 20 | } 21 | } 22 | 23 | function updateUITheme() { 24 | var darkModeMenu = getDarkModeMenuItem(); 25 | 26 | if(darkModeMenu.checked) { 27 | setPreference('darkmode', true); 28 | setUIDark(); 29 | } else { 30 | setPreference('darkmode', false); 31 | setUILight(); 32 | } 33 | } 34 | 35 | function changeThemeMode() { 36 | let darkModeMenu = getDarkModeMenuItem(); 37 | darkModeMenu.checked = !getPreference('darkmode'); 38 | } 39 | 40 | // --------------------------------------------------------------------------------------------------------------------- 41 | 42 | function getDarkModeMenuItem() { 43 | // NOTE: Go through all menu items 44 | // NOTE: Find the "Dark Mode" menu item 45 | 46 | let menuItem = null; 47 | 48 | for (let i in Menu.getApplicationMenu().items) { 49 | appMenuItem = Menu.getApplicationMenu().items[i]; 50 | 51 | for (let j in appMenuItem.submenu.items) { 52 | if (appMenuItem.submenu.items[j].label == i18n.__('Dark Mode') && appMenuItem.submenu.items[j].type == "checkbox") { 53 | menuItem = appMenuItem.submenu.items[j]; 54 | break; 55 | } 56 | } 57 | 58 | if (menuItem != null) 59 | break; 60 | } 61 | 62 | return menuItem; 63 | } 64 | -------------------------------------------------------------------------------- /app/js/episode_class.js: -------------------------------------------------------------------------------- 1 | class Episode { 2 | constructor(ChannelName, FeedUrl, EpisodeTitle, EpisodeUrl, EpisodeType, EpisodeLength, EpisodeDescription, DurationKey, pubDate, artwork) { 3 | this.channelName = ChannelName; 4 | this.feedUrl = FeedUrl; 5 | this.episodeTitle = EpisodeTitle; 6 | this.episodeUrl = EpisodeUrl; 7 | this.episodeType = EpisodeType; 8 | this.episodeLength = EpisodeLength; 9 | this.episodeDescription = EpisodeDescription; 10 | this.durationKey = DurationKey; 11 | this.pubDate = pubDate; 12 | this.artwork = artwork; 13 | } 14 | } -------------------------------------------------------------------------------- /app/js/favorite.js: -------------------------------------------------------------------------------- 1 | var allFavoritePodcasts = null; 2 | 3 | class FavoritePodcastsUI { 4 | constructor() { 5 | } 6 | 7 | isFavoritesPage() { 8 | if(getHeader() == generateHtmlTitle('Favorites')) 9 | return true; 10 | return false; 11 | } 12 | 13 | isEmpty() { 14 | return !this.getAllItemsList().get(0); 15 | } 16 | 17 | getList() { 18 | return $('#list'); 19 | } 20 | 21 | getAllItemsList() { 22 | return $('#list li'); 23 | } 24 | 25 | getByFeedUrl(feedUrl) { 26 | return this.getList().find('.podcast-entry-header[feedurl="' + feedUrl + '"]').parent(); 27 | } 28 | 29 | add() { 30 | setItemCounts(); 31 | } 32 | 33 | showNothingToShowPage() { 34 | if(this.isFavoritesPage()) { 35 | setHeaderViewAction(""); 36 | setNothingToShowBody(s_FavoritesNothingFoundIcon, 'favorites-nothing-to-show'); 37 | } 38 | } 39 | 40 | removeByFeedUrl(feedUrl) { 41 | if(this.isFavoritesPage()) { 42 | let $podcast = this.getByFeedUrl(feedUrl); 43 | $podcast 44 | .animate({opacity: 0.0}, 150) 45 | .slideUp(150, () => { 46 | $podcast.remove(); 47 | 48 | if(this.isEmpty()) 49 | this.showNothingToShowPage(); 50 | }); 51 | 52 | } 53 | setItemCounts(); 54 | } 55 | } 56 | 57 | class FavoritePodcasts { 58 | constructor() { 59 | this.load(); 60 | this.ui = new FavoritePodcastsUI(); 61 | } 62 | 63 | load() { 64 | if (!fs.existsSync(getSaveFilePath())) 65 | fs.openSync(getSaveFilePath(), 'w'); 66 | 67 | let fileContent = ifExistsReadFile(getSaveFilePath()); 68 | this.podcasts = JSON.parse(fileContent == "" ? "[]": fileContent); 69 | } 70 | 71 | update() { 72 | fs.writeFileSync(getSaveFilePath(), JSON.stringify(this.podcasts, null, "\t")); 73 | } 74 | 75 | length() { 76 | return this.podcasts.length; 77 | } 78 | 79 | isEmpty() { 80 | return (this.length() == 0); 81 | } 82 | 83 | getAll() { 84 | return this.podcasts; 85 | } 86 | 87 | exists(feedUrl) { 88 | return Boolean(this.getByFeedUrl(feedUrl)); 89 | } 90 | 91 | findByFeedUrl(feedUrl) { 92 | for(let i in this.podcasts) 93 | if(this.podcasts[i].feedUrl == feedUrl) 94 | return i; 95 | return -1; 96 | } 97 | 98 | getByFeedUrl(feedUrl) { 99 | let i = this.findByFeedUrl(feedUrl); 100 | return (i != -1 ? this.podcasts[i] : undefined); 101 | } 102 | 103 | setData(podcast) { 104 | let i = this.findByFeedUrl(podcast.feedUrl); 105 | if(i != -1) { 106 | let oldArtwork = this.podcasts[i].data.artworkUrl; 107 | this.podcasts[i].data = {...podcast.data}; 108 | this.podcasts[i].data.artworkUrl = oldArtwork; 109 | this.update(); 110 | return true; 111 | } 112 | return false; 113 | } 114 | 115 | add(podcast) { 116 | let p = Object.assign(new Podcast, podcast); 117 | if(p.isValid() && this.findByFeedUrl(podcast.feedUrl) == -1) { 118 | let i = 0; 119 | while(i < this.podcasts.length && podcast.data.collectionName.localeCompare(this.podcasts[i].data.collectionName) == 1) 120 | i++; 121 | this.podcasts.splice(i, 0, podcast); 122 | this.update(); 123 | this.ui.add(); 124 | 125 | let feed = null; 126 | if(allFeeds.ui.checkPageByFeedUrl(podcast.feedUrl) && (feed = allFeeds.ui.getData())) { 127 | allFeeds.initFeed(podcast.feedUrl); 128 | allFeeds.set(feed); 129 | addEpisodesFromTheLastWeek(podcast.feedUrl, feed); 130 | } else 131 | readFeedByFeedUrl(podcast.feedUrl); 132 | return podcast; 133 | } 134 | return null; 135 | } 136 | 137 | removeByFeedUrl(feedUrl) { 138 | let i = this.findByFeedUrl(feedUrl); 139 | if(i != -1) { 140 | this.podcasts.splice(i, 1); 141 | this.update(); 142 | allNewEpisodes.removePodcastEpisodes(feedUrl); 143 | allFeeds.delete(feedUrl); 144 | 145 | this.ui.removeByFeedUrl(feedUrl); 146 | 147 | return true; 148 | } 149 | return false; 150 | } 151 | 152 | setExcludeFromNewEpisodesByFeedUrl(feedUrl, value) { 153 | let i = this.findByFeedUrl(feedUrl); 154 | if(i != -1) { 155 | this.podcasts[i].excludeFromNewEpisodes = value; 156 | this.update(); 157 | return true; 158 | } 159 | return false; 160 | } 161 | 162 | getExcludeFromNewEpisodesByFeedUrl(feedUrl) { 163 | let i = this.findByFeedUrl(feedUrl); 164 | if(i != -1) 165 | return Boolean(this.podcasts[i].excludeFromNewEpisodes); 166 | return true; 167 | } 168 | } 169 | 170 | function loadFavoritePodcasts() { 171 | allFavoritePodcasts = new FavoritePodcasts(); 172 | } 173 | 174 | 175 | function setFavorite(_Self, _ArtistName, _CollectioName, _Artwork, _FeedUrl, description) { 176 | let podcast = new Podcast( 177 | _ArtistName, 178 | _CollectioName, 179 | _Artwork, 180 | _FeedUrl, 181 | description 182 | ); 183 | 184 | let $podcastRow = $(_Self).parent().parent(); 185 | $(_Self).parent().remove(); 186 | $podcastRow.append(getFullHeartButton(podcast)); 187 | console.log(podcast) 188 | allFavoritePodcasts.add(podcast); 189 | 190 | } 191 | 192 | function unsetFavorite(_Self, _ArtistName, _CollectioName, _Artwork, _FeedUrl, description) { 193 | let podcast = new Podcast( 194 | _ArtistName, 195 | _CollectioName, 196 | _Artwork, 197 | _FeedUrl, 198 | description 199 | ); 200 | 201 | allFavoritePodcasts.removeByFeedUrl(_FeedUrl); 202 | 203 | let $podcastRow = $(_Self).parent().parent(); 204 | $(_Self).parent().remove(); 205 | $podcastRow.append(getHeartButton(podcast)); 206 | } -------------------------------------------------------------------------------- /app/js/feed.js: -------------------------------------------------------------------------------- 1 | process.dlopen = () => { 2 | throw new Error('Load native module is not safe') 3 | } 4 | const xmlParserWorker = new Worker('./js/xmlparser_worker.js') 5 | const updateFeedWorker = new Worker('./js/update_feed_worker.js') 6 | 7 | setInterval(function () { 8 | readFeeds(); 9 | console.log('Feeds have been read!'); 10 | }, 30 * 60 * 1000); 11 | 12 | function checkDateIsInTheLastWeek(episode) { 13 | var day = new Date(); 14 | var previousweek = day.getTime() - 7 * 24 * 60 * 60 * 1000; 15 | 16 | return (compareEpisodeDates(episode, {pubDate: previousweek}) > 0); 17 | } 18 | 19 | function compareEpisodeDates(episode1, episode2) { 20 | if(episode1 && episode2) { 21 | let pubDate1 = (episode1.pubDate ? episode1.pubDate : getInfoEpisodeByObj(episode1).pubDate); 22 | let pubDate2 = (episode2.pubDate ? episode2.pubDate : getInfoEpisodeByObj(episode2).pubDate); 23 | let date1 = new Date(pubDate1); 24 | let date2 = new Date(pubDate2); 25 | if(date1.getTime() < date2.getTime()) 26 | return -1; 27 | if(date1.getTime() > date2.getTime()) 28 | return 1; 29 | return 0; 30 | } 31 | return undefined; 32 | } 33 | 34 | 35 | function getInfoEpisodeByObj(episode) { 36 | let feedUrl = episode.feedUrl; 37 | let episodeUrl = episode.episodeUrl; 38 | if(feedUrl && episodeUrl) 39 | return allFeeds.getEpisodeByEpisodeUrl(feedUrl, episodeUrl); 40 | return undefined; 41 | } 42 | 43 | function urlify(text) { 44 | let urlRegex = /(https?:\/\/)?[\w\-@~]+(\.[\w\-~]+)+(\/[\w\-~@:%]*)*(#[\w\-]*)?(\?[^\s]*)?/gi; 45 | return text.replace(urlRegex, function (url) { 46 | if(url.length <= 3) 47 | return url; 48 | let content = url; 49 | if(url.indexOf('@') != -1) 50 | url = 'mailto:' + url; 51 | else if(url.substr(0, 4) != 'http') 52 | url = 'http://' + url; 53 | return '' + content + ''; 54 | }) 55 | } 56 | 57 | function getInfoFromDescription(episodeDescription) { 58 | episodeDescription = episodeDescription.replaceAll('\n
', '
') 59 | .replaceAll('
\n', '
') 60 | .replaceAll('\n', '
'); 61 | return (episodeDescription.indexOf('') == -1 ? urlify(episodeDescription) : episodeDescription); 62 | } 63 | 64 | function getDurationFromDurationKey(episode) { 65 | if(!episode.durationKey) 66 | return "#h #min"; 67 | 68 | let duration = parseFeedEpisodeDuration(episode.durationKey.split(":")); 69 | 70 | if (duration.hours == 0 && duration.minutes == 0) 71 | duration = ""; 72 | else 73 | duration = duration.hours + "h " + duration.minutes + "min"; 74 | return duration; 75 | } 76 | 77 | function setRefreshingStateUI() { 78 | $('#menu-refresh').addClass('is-refreshing'); 79 | $('#menu-refresh').off('click'); 80 | } 81 | 82 | function unsetRefreshingStateUI() { 83 | setTimeout(() => { 84 | $('#menu-refresh').removeClass('is-refreshing'); 85 | $('#menu-refresh').click(readFeeds); 86 | }, 2000); 87 | } 88 | 89 | function readFeeds() { 90 | setRefreshingStateUI(); 91 | 92 | if(!allFavoritePodcasts.isEmpty()) { 93 | let podcasts = allFavoritePodcasts.getAll(); 94 | for (let i in podcasts) { 95 | allFeeds.lastFeedUrlToReload = podcasts[i].feedUrl; 96 | readFeedByFeedUrl(podcasts[i].feedUrl, (i == podcasts.length - 1)); 97 | } 98 | } else 99 | unsetRefreshingStateUI(); 100 | allArchiveEpisodes.downloadManager.saveAll(); 101 | } 102 | 103 | function readFeedByFeedUrl(feedUrl, forceUnsetRefreshing) { 104 | makeRequest(feedUrl, updateFeed, (jqXHR) => { 105 | // ERR_INTERNET_DISCONNECTED 106 | if(jqXHR.status == 0 || forceUnsetRefreshing) 107 | unsetRefreshingStateUI(); 108 | }); 109 | } 110 | 111 | function updateFeed(_Content, FeedUrl) { 112 | allFeeds.initFeed(FeedUrl) 113 | 114 | if (isContent302NotFound(_Content)) { 115 | allFeeds.ui.showNothingToShow(FeedUrl); 116 | } else { 117 | if (_Content.includes("")) 118 | allFeeds.ui.showNothingToShow(FeedUrl); 119 | else 120 | processEpisodes(_Content, FeedUrl); 121 | } 122 | } 123 | 124 | xmlParserWorker.onmessage = function(ev) { 125 | let newFeed = ev.data.json; 126 | let podcastData = ev.data.podcastData; 127 | let feedUrl = podcastData.feedUrl; 128 | let oldFeed = allFeeds.getFeedPodcast(feedUrl); 129 | oldFeed = (!oldFeed ? [] : oldFeed); 130 | 131 | allFavoritePodcasts.setData(podcastData); 132 | allFeeds.set(newFeed); 133 | 134 | if(allFeeds.ui.checkPageByFeedUrl(feedUrl)) { 135 | allFeeds.ui.setHeaderArtistContent(podcastData.data.artistName); 136 | allFeeds.ui.setHeaderDescriptionContent(getInfoFromDescription(podcastData ? podcastData.data.description : '')); 137 | } 138 | 139 | updateFeedWorker.postMessage({ 140 | oldFeed: oldFeed, 141 | newFeed: newFeed 142 | }) 143 | 144 | if(allFeeds.lastFeedUrlToReload == feedUrl) 145 | unsetRefreshingStateUI(); 146 | } 147 | 148 | updateFeedWorker.onmessage = function(ev) { 149 | let feedUrl = ev.data.feedUrl; 150 | let new_episodes = ev.data.new_episodes; 151 | let deleted_episodes = ev.data.deleted_episodes; 152 | let initialLength = ev.data.initialLength; 153 | let feed = ev.data.feed; 154 | 155 | if(initialLength == 0 && new_episodes.length == feed.length) { 156 | allFeeds.ui.showLastNFeedElements(feed); 157 | addEpisodesFromTheLastWeek(feedUrl, feed); 158 | } else { 159 | for(let i in new_episodes) { 160 | let index = allFeeds.findEpisodeByEpisodeUrl(feedUrl, new_episodes[i]) 161 | let episode = feed[index]; 162 | 163 | allFeeds.ui.add(episode, index); 164 | if(!getSettings(feedUrl) && checkDateIsInTheLastWeek(episode)) 165 | allNewEpisodes.add(episode); 166 | } 167 | } 168 | 169 | for(let i = deleted_episodes.length - 1; i >= 0; i--) { 170 | let episodeUrl = deleted_episodes[i]; 171 | 172 | allFeeds.ui.remove(feedUrl, episodeUrl); 173 | allFeeds.playback.remove(episodeUrl) 174 | allNewEpisodes.removeByEpisodeUrl(episodeUrl); 175 | allArchiveEpisodes.removeByEpisodeUrl(episodeUrl); 176 | } 177 | } 178 | 179 | function addEpisodesFromTheLastWeek(feedUrl, feed) { 180 | if(!getSettings(feedUrl)) { 181 | for(let i in feed) { 182 | let episode = feed[i]; 183 | if(checkDateIsInTheLastWeek(episode)) 184 | allNewEpisodes.add(episode); 185 | else 186 | return; 187 | } 188 | } 189 | } 190 | 191 | function showAllEpisodes(obj) { 192 | let podcast = _(obj); 193 | 194 | let tmpPodcast = allFavoritePodcasts.getByFeedUrl(podcast.feedUrl); 195 | if(tmpPodcast) 196 | podcast = tmpPodcast; 197 | if(!podcast.data) 198 | podcast = getPodcastFromEpisode(podcast); 199 | 200 | setGridLayout(false); 201 | 202 | clearBody(); 203 | setHeaderViewAction(); 204 | removeContentRightHeader(); 205 | 206 | getAllEpisodesFromFeed(podcast); 207 | } 208 | 209 | function getAllEpisodesFromFeed(podcast) { 210 | let feedUrl = podcast.feedUrl; 211 | 212 | allFeeds.ui.showHeader(podcast); 213 | 214 | let feed = allFeeds.getFeedPodcast(feedUrl); 215 | allFeeds.ui.showLastNFeedElements(feed); 216 | 217 | makeRequest(feedUrl, processEpisodes, () => { 218 | allFeeds.ui.showNothingToShow(feedUrl); 219 | }); 220 | } 221 | 222 | // --------------------------------------------------------------------------------------------------------------------- 223 | // NOTE: Helper to clear corrupt feeds 224 | 225 | function isContent302NotFound(content) { 226 | return (content == "" || content.includes("302 Found")); 227 | } 228 | 229 | // --------------------------------------------------------------------------------------------------------------------- 230 | 231 | function processEpisodes(content, feedUrl) { 232 | xmlParserWorker.postMessage({ 233 | xml: content, 234 | feedUrl: feedUrl, 235 | artwork: getBestArtworkUrl(feedUrl) 236 | }); 237 | } 238 | 239 | function addToArchive(self) { 240 | let listElement = self.parentElement.parentElement; 241 | 242 | allArchiveEpisodes.add(_(listElement)); 243 | 244 | } 245 | 246 | function removeFromArchive(self) { 247 | let listElement = self.parentElement.parentElement; 248 | 249 | allArchiveEpisodes.removeByEpisodeUrl(_(listElement).episodeUrl); 250 | } -------------------------------------------------------------------------------- /app/js/helper/helper_entries.js: -------------------------------------------------------------------------------- 1 | 2 | // --------------------------------------------------------------------------------------------------------------------- 3 | // RIGHT COLUMN 4 | // --------------------------------------------------------------------------------------------------------------------- 5 | 6 | 7 | function unsubscribeListElement(self) { 8 | let feedUrl = $(self).parent().data().feedUrl; 9 | allFavoritePodcasts.removeByFeedUrl(feedUrl); 10 | } 11 | 12 | function unsubscribeContextMenu(feedUrl) { 13 | allFavoritePodcasts.removeByFeedUrl(feedUrl); 14 | showFavoritesPage(); 15 | } 16 | 17 | function setHeartContent(self, emptyHeart) { 18 | $(self) 19 | .stop() 20 | .removeAttr('style') 21 | 22 | if(emptyHeart) { 23 | $(self).animate( 24 | {opacity: 0.4}, 25 | 150, 26 | function() { 27 | $(self) 28 | .html($(s_Heart).html()) 29 | .animate( 30 | {opacity: 0.6}, 31 | 150, 32 | function () { 33 | $(self).removeAttr('style'); 34 | }); 35 | } 36 | ); 37 | } else 38 | $(self).html($(s_FullHeart).html()); 39 | } 40 | 41 | // --------------------------------------------------------------------------------------------------------------------- 42 | // PODCAST ENTRY 43 | // --------------------------------------------------------------------------------------------------------------------- 44 | 45 | function getPodcastElement(artwork, title) { 46 | return $(` 47 |
  • 48 |
    49 | 50 |
    ${title}
    51 |
    52 |
    53 | ${s_FullHeart} 54 |
    55 |
  • 56 | `).get(0); 57 | } 58 | 59 | function getStatisticsHeaderElement(title) { 60 | return $(` 61 |
  • 62 |
    ${title}
    63 |
  • 64 | `).get(0); 65 | } 66 | 67 | function getStatisticsEntryElement(title, value) { 68 | return $(` 69 |
  • 70 |
    ${title}
    71 |
    ${value}
    72 |
  • 73 | `).get(0); 74 | } 75 | -------------------------------------------------------------------------------- /app/js/helper/helper_navigation.js: -------------------------------------------------------------------------------- 1 | // --------------------------------------------------------------------------------------------------------------------- 2 | // LEFT COLUMN 3 | // --------------------------------------------------------------------------------------------------------------------- 4 | 5 | function setItemCounts() { 6 | $('#menu-episodes .menu-count').html(allNewEpisodes.length() > allNewEpisodes.ui.bufferSize ? 7 | allNewEpisodes.ui.bufferSize : allNewEpisodes.length()); 8 | $('#menu-favorites .menu-count').html(allFavoritePodcasts.length()); 9 | } 10 | 11 | function setGridLayout(enable) { 12 | if (enable) 13 | $('#list').addClass("grid-layout"); 14 | else 15 | $('#list').removeClass("grid-layout"); 16 | } 17 | 18 | // --------------------------------------------------------------------------------------------------------------------- 19 | // RIGHT COLUMN 20 | // --------------------------------------------------------------------------------------------------------------------- 21 | 22 | function setHeaderViewAction(_Mode) { 23 | let $content_right_header_actions = $('#content-right-header-actions'); 24 | switch (_Mode) { 25 | case "list": 26 | $content_right_header_actions.html(s_ListView); 27 | $content_right_header_actions.find('svg').click(function () { 28 | toggleList('list'); 29 | }); 30 | break; 31 | 32 | case "grid": 33 | $content_right_header_actions.html(s_GridView); 34 | $content_right_header_actions.find('svg').click(function () { 35 | toggleList('grid'); 36 | }); 37 | break; 38 | 39 | default: 40 | $content_right_header_actions.html(''); 41 | break; 42 | } 43 | } 44 | 45 | function toggleList(_View) { 46 | switch (_View) { 47 | case "list": 48 | setGridLayout(false) 49 | setHeaderViewAction("grid") 50 | break; 51 | 52 | case "grid": 53 | setGridLayout(true) 54 | setHeaderViewAction("list") 55 | break; 56 | 57 | default: 58 | break; 59 | } 60 | } 61 | 62 | // --------------------------------------------------------------------------------------------------------------------- 63 | // MENU 64 | // --------------------------------------------------------------------------------------------------------------------- 65 | 66 | function clearMenuSelection() { 67 | $('#menu li').removeClass('selected'); 68 | } -------------------------------------------------------------------------------- /app/js/lib/jquery.animateRotate.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery') 2 | $.fn.animateRotate = function(angle, duration, easing, complete) { 3 | var args = $.speed(duration, easing, complete); 4 | var step = args.step; 5 | return this.each(function(i, e) { 6 | args.complete = $.proxy(args.complete, e); 7 | args.step = function(now) { 8 | $.style(e, 'transform', 'rotate(' + now + 'deg)'); 9 | if (step) return step.apply(e, arguments); 10 | }; 11 | 12 | $({deg: 0}).animate({deg: angle}, args); 13 | }); 14 | }; -------------------------------------------------------------------------------- /app/js/lib/jquery.hoverIntent.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * hoverIntent v1.10.1 // 2019.10.05 // jQuery v1.7.0+ 3 | * http://briancherne.github.io/jquery-hoverIntent/ 4 | * 5 | * You may use hoverIntent under the terms of the MIT license. Basically that 6 | * means you are free to use hoverIntent as long as this header is left intact. 7 | * Copyright 2007-2019 Brian Cherne 8 | */ 9 | !function(factory){"use strict";"function"==typeof define&&define.amd?define(["jquery"],factory):"object"==typeof module&&module.exports?module.exports=factory(require("jquery")):jQuery&&!jQuery.fn.hoverIntent&&factory(jQuery)}(function($){"use strict";function track(ev){cX=ev.pageX,cY=ev.pageY}var cX,cY,_cfg={interval:100,sensitivity:6,timeout:0},INSTANCE_COUNT=0,compare=function(ev,$el,s,cfg){if(Math.sqrt((s.pX-cX)*(s.pX-cX)+(s.pY-cY)*(s.pY-cY)) this.bufferSize) 25 | this.removeByEpisodeUrl(this.getLastItemList().attr('url')); 26 | } 27 | } 28 | } 29 | 30 | directAdd(episode, i) { 31 | let $el = null; 32 | if(!$(this.getAllItemsList().get(i)).get(0)) { 33 | if(this.isEmpty()) 34 | $('#list > .nothing-to-show').remove() 35 | $el = this.getShowMoreEpisodesBottomElement(); 36 | } else 37 | $el = $(this.getAllItemsList().get(i)); 38 | 39 | this.getNewItemList(episode) 40 | .hide() 41 | .css('opacity', 0.0) 42 | .insertBefore($el) 43 | .slideDown('slow') 44 | .animate({opacity: 1.0}); 45 | } 46 | 47 | removeByEpisodeUrl(episodeUrl, feed) { 48 | if(this.getByEpisodeUrl(episodeUrl).get(0)) { 49 | this.lastEpisodeDisplayed--; 50 | let $episodeItem = this.getByEpisodeUrl(episodeUrl); 51 | 52 | $episodeItem 53 | .animate({opacity: 0.0}, 150) 54 | .slideUp(150, () => { 55 | $episodeItem.remove(); 56 | 57 | if(this.length() < this.bufferSize) 58 | if(feed) { 59 | let episodeToAdd = feed[this.lastEpisodeDisplayed + 1]; 60 | if(episodeToAdd) { 61 | this.directBottomAdd(episodeToAdd); 62 | this.lastEpisodeDisplayed++; 63 | if(this.lastEpisodeDisplayed == feed.length - 1) 64 | this.getShowMoreEpisodesBottomElement().hide(); 65 | return; 66 | } 67 | 68 | episodeToAdd = feed[this.firstEpisodeDisplayed - 1]; 69 | if(episodeToAdd) { 70 | this.directTopAdd(episodeToAdd); 71 | this.firstEpisodeDisplayed--; 72 | if(this.firstEpisodeDisplayed == 0) 73 | this.getShowMoreEpisodesTopElement().hide(); 74 | return; 75 | } 76 | } 77 | 78 | this.showNothingToShow(); 79 | }); 80 | } 81 | } 82 | 83 | directBottomAdd(episode) { 84 | this.getNewItemList(episode) 85 | .hide() 86 | .css('opacity', 0.0) 87 | .insertAfter(this.getLastItemList()) 88 | .slideDown(150) 89 | .animate({opacity: 1.0}); 90 | } 91 | 92 | directTopAdd(episode) { 93 | this.getNewItemList(episode) 94 | .hide() 95 | .css('opacity', 0.0) 96 | .insertBefore(this.getFirstItemList()) 97 | .slideDown(150) 98 | .animate({opacity: 1.0}); 99 | } 100 | 101 | showNothingToShow(_icon, _class) { 102 | if(this.isEmpty()) 103 | setNothingToShowBody(_icon, _class); 104 | } 105 | 106 | showList(feed) { 107 | if(feed.length == 0) 108 | this.showNothingToShow(); 109 | 110 | let $list = this.getList(); 111 | 112 | let length = (feed.length < this.bufferSize ? feed.length : this.bufferSize); 113 | 114 | for (let i = 0; i < length; i++) 115 | $list.append(this.getNewItemList(feed[i])); 116 | 117 | this.firstEpisodeDisplayed = 0; 118 | this.lastEpisodeDisplayed = length - 1; 119 | 120 | this.appendShowMoreEpisodesButton(); 121 | this.prependShowMoreEpisodesButton(); 122 | 123 | if(length != feed.length) 124 | this.getShowMoreEpisodesBottomElement().show(); 125 | 126 | setScrollPositionOnTop(); 127 | } 128 | 129 | convertItemIntoInfoItemList(obj) { 130 | } 131 | 132 | convertInfoItemIntoItemList(obj) { 133 | } 134 | 135 | getNewItemList(episode) { 136 | } 137 | 138 | lastEpisodeIsDisplayed(feed) { 139 | return (this.lastEpisodeDisplayed == feed.length - 1); 140 | } 141 | 142 | firstEpisodeIsDisplayed() { 143 | return (this.firstEpisodeDisplayed == 0); 144 | } 145 | 146 | getShowMoreEpisodesHtml(text, className) { 147 | if(!className) 148 | className = ''; 149 | return ''; 154 | } 155 | 156 | getShowMoreEpisodesBottomHtml() { 157 | return this.getShowMoreEpisodesHtml('Show more episodes', 'more-episodes-bottom'); 158 | } 159 | 160 | getShowMoreEpisodesTopHtml() { 161 | return this.getShowMoreEpisodesHtml('Show more recent episodes', 'more-episodes-top'); 162 | } 163 | 164 | getShowMoreEpisodesBottomElement() { 165 | return this.getList().find('.more-episodes-bottom'); 166 | } 167 | 168 | getShowMoreEpisodesTopElement() { 169 | return this.getList().find('.more-episodes-top'); 170 | } 171 | 172 | showOther10Elements(feed) { 173 | let i = this.lastEpisodeDisplayed + 1; 174 | let delay = 0; 175 | while(i < feed.length && i < this.lastEpisodeDisplayed + 11) { 176 | let episode = this.dataObject.getByEpisodeUrl(feed[i].episodeUrl); 177 | this.getNewItemList(episode) 178 | .delay(140 * delay++) 179 | .hide() 180 | .css('opacity', 0.0) 181 | .insertBefore(this.getShowMoreEpisodesBottomElement()) 182 | .slideDown(100,function () { 183 | $(this).animate({opacity: 1.0}); 184 | }); 185 | 186 | i++; 187 | } 188 | this.lastEpisodeDisplayed = i - 1; 189 | } 190 | 191 | showsPrevious10Elements(feed) { 192 | let i = this.firstEpisodeDisplayed - 1; 193 | let delay = 0; 194 | while(i >= 0 && i >= this.firstEpisodeDisplayed - 10) { 195 | let episode = this.dataObject.getByEpisodeUrl(feed[i].episodeUrl); 196 | this.getNewItemList(episode) 197 | .delay(140 * delay++) 198 | .hide() 199 | .css('opacity', 0.0) 200 | .insertBefore(this.getFirstItemList()) 201 | .slideDown(100, function () { 202 | $(this).animate({opacity: 1.0}, 200); 203 | }); 204 | 205 | i--; 206 | } 207 | this.firstEpisodeDisplayed = i + 1; 208 | } 209 | 210 | removeExtraPreviousElements() { 211 | let nElement = this.getAllItemsList().length - this.bufferSize; 212 | this.getAllItemsList() 213 | .slice(0, nElement) 214 | .remove(); 215 | this.firstEpisodeDisplayed += nElement; 216 | 217 | this.getShowMoreEpisodesTopElement().show(); 218 | } 219 | 220 | removeExtraNextElements() { 221 | let nElement = this.bufferSize - this.getAllItemsList().length; 222 | this.getAllItemsList() 223 | .slice(nElement) 224 | .remove(); 225 | 226 | this.lastEpisodeDisplayed += nElement; 227 | 228 | this.getShowMoreEpisodesBottomElement().show(); 229 | } 230 | 231 | appendShowMoreEpisodesButton() { 232 | this.getList() 233 | .append(this.getShowMoreEpisodesBottomHtml()) 234 | 235 | function clickFunction(obj) { 236 | let $button = obj.getList().find('.more-episodes-bottom').find('.show-more-episodes-button'); 237 | $button.off('click'); 238 | 239 | let feed = obj.dataObject.getAll();/* this.dataObject.getAll(); */ 240 | 241 | obj.showOther10Elements(feed); 242 | obj.removeExtraPreviousElements(); 243 | console.log("_ ", obj.firstEpisodeDisplayed, obj.lastEpisodeDisplayed) 244 | 245 | if(obj.lastEpisodeIsDisplayed(feed)) { 246 | let $showMoreEpisodesRowBottom = obj.getShowMoreEpisodesBottomElement(); 247 | 248 | $showMoreEpisodesRowBottom 249 | .css('opacity', 0.7) 250 | .animate({opacity: 0.0}, 150) 251 | .slideUp(150, () => { 252 | $showMoreEpisodesRowBottom.hide(); 253 | $showMoreEpisodesRowBottom.css('opacity', ''); 254 | 255 | $button.click(() => { 256 | clickFunction(obj); 257 | }); 258 | }); 259 | } else { 260 | $button.click(() => { 261 | clickFunction(obj); 262 | }); 263 | } 264 | } 265 | 266 | this.getList().find('.more-episodes-bottom').find('.show-more-episodes-button') 267 | .click(() => { 268 | clickFunction(this); 269 | }); 270 | } 271 | 272 | prependShowMoreEpisodesButton() { 273 | this.getList() 274 | .prepend(this.getShowMoreEpisodesTopHtml()) 275 | 276 | function clickFunction(obj) { 277 | let $button = obj.getList().find('.more-episodes-top').find('.show-more-episodes-button'); 278 | $button.off('click'); 279 | 280 | let feed = obj.dataObject.getAll(); /* this.dataObject.getAll(); */ 281 | 282 | obj.showsPrevious10Elements(feed); 283 | let timeout = 130 * (obj.getAllItemsList().length - obj.bufferSize); 284 | obj.removeExtraNextElements(); 285 | console.log("^", obj.firstEpisodeDisplayed, obj.lastEpisodeDisplayed) 286 | 287 | if(obj.firstEpisodeIsDisplayed()) { 288 | let $showMoreEpisodesRowTop = obj.getShowMoreEpisodesTopElement(); 289 | 290 | $showMoreEpisodesRowTop 291 | .css('opacity', 0.7) 292 | .animate({opacity: 0.0}, 150) 293 | .slideUp(150, () => { 294 | $showMoreEpisodesRowTop.hide(); 295 | $showMoreEpisodesRowTop.css('opacity', ''); 296 | 297 | setTimeout(() => { 298 | $button.click(() => { 299 | clickFunction(obj); 300 | }); 301 | }, timeout); 302 | }); 303 | } else { 304 | setTimeout(() => { 305 | $button.click(() => { 306 | clickFunction(obj); 307 | }); 308 | }, timeout); 309 | } 310 | } 311 | 312 | this.getList().find('.more-episodes-top').find('.show-more-episodes-button') 313 | .click(() => { 314 | clickFunction(this); 315 | }); 316 | } 317 | } -------------------------------------------------------------------------------- /app/js/list_item.js: -------------------------------------------------------------------------------- 1 | 2 | function buildListItem(partsArray, layoutRatio) { 3 | let $container = document.createElement("li"); 4 | 5 | for(let i = 0; i < partsArray.length; i++) 6 | $container.append(partsArray[i]); 7 | 8 | $container.classList.add('list-item-row-layout'); 9 | $container.style.gridTemplateColumns = layoutRatio; 10 | 11 | return $container; 12 | } 13 | 14 | function getImagePart(artwork) { 15 | let $imageElement = document.createElement("img"); 16 | 17 | $imageElement.src = artwork 18 | $imageElement.style.backgroundImage = "url(./img/podcast_07prct.svg)"; 19 | $imageElement.style.backgroundRepeat = "no-repeat"; 20 | $imageElement.style.backgroundSize = "cover"; 21 | 22 | return $imageElement; 23 | } 24 | 25 | function getGenericPart(innerHTML, elementClass) { 26 | return $( 27 | `
    28 | ${innerHTML} 29 |
    ` 30 | ).get(0); 31 | } 32 | 33 | function getBoldTextPart(text) { 34 | return getGenericPart(text, "list-item-bold-text"); 35 | } 36 | 37 | function getTextPart(text) { 38 | return getGenericPart(text, "list-item-text"); 39 | } 40 | 41 | function getSubTextPart(text) { 42 | return getGenericPart(text, "list-item-sub-text"); 43 | } 44 | 45 | function getFlagPart(text) { 46 | return $(getGenericPart(text, "list-item-flag")); 47 | } 48 | 49 | function getProgressionFlagPart(episodeUrl) { 50 | return allFeeds.playback.ui.getProgressionFlag(episodeUrl); 51 | } 52 | 53 | function getIconButtonPart(icon) { 54 | return getGenericPart(icon, "list-item-icon"); 55 | } 56 | 57 | function getDescriptionPart() { 58 | return getGenericPart(s_InfoIcon, "list-item-icon list-item-description"); 59 | } 60 | 61 | function addToArchiveOnClickAction(event) { 62 | event.stopPropagation(); 63 | 64 | let episodeUrl = $(this).parent().parent().attr('url'); 65 | let stateEpisode = allArchiveEpisodes.getStateDownload(episodeUrl); 66 | if(stateEpisode === 'completed' || stateEpisode === 'error' || stateEpisode === 'in_progress') { 67 | removeFromArchive(this); 68 | allArchiveEpisodes.ui.setNotDownloadedYet(episodeUrl); 69 | } else 70 | addToArchive(this); 71 | } 72 | 73 | function getAddToArchiveButtonPart(episodeUrl) { 74 | let stateEpisode = allArchiveEpisodes.getStateDownload(episodeUrl); 75 | 76 | let $button = null; 77 | switch(stateEpisode) { 78 | case 'completed': 79 | $button = getIconButtonPart(allArchiveEpisodes.ui.isArchivePage() ? s_DeleteIcon : s_RemoveEpisodeIcon); 80 | $button.title = i18n.__("Remove from archive"); 81 | break; 82 | case 'error': 83 | $button = getIconButtonPart(s_DownloadErrorIcon); 84 | $button.title = i18n.__("Download error"); 85 | break; 86 | case 'in_progress': 87 | $button = getIconButtonPart(s_DownloadInProgressIcon); 88 | $button.title = i18n.__("Download in progress"); 89 | break; 90 | default: 91 | $button = getIconButtonPart(s_AddEpisodeIcon); 92 | $button.title = i18n.__("Add to archive"); 93 | break; 94 | } 95 | 96 | $($button).find('svg').on('click', addToArchiveOnClickAction); 97 | return $button; 98 | } 99 | 100 | function changeIconButton(obj, icon, title) { 101 | $(obj) 102 | .off('click') 103 | .stop() 104 | .animate( 105 | {opacity: 0.6}, 106 | 120, 107 | function() { 108 | $(obj) 109 | .html($(icon).html()) 110 | .animate( 111 | {opacity: 1.0}, 112 | 120, 113 | function () { 114 | $(obj) 115 | .on('click', addToArchiveOnClickAction) 116 | .removeAttr('style') 117 | .parent() 118 | .attr('title', title); 119 | }); 120 | } 121 | ); 122 | } -------------------------------------------------------------------------------- /app/js/menu.js: -------------------------------------------------------------------------------- 1 | 2 | function selectMenuItem(_MenuId) { 3 | let $menuItem = _MenuId; 4 | 5 | clearTextField($('#search-input').get(0)); 6 | loseFocusTextField("search-input"); 7 | 8 | clearMenuSelection(); 9 | 10 | $menuItem.addClass("selected"); 11 | } 12 | 13 | function showNewEpisodesPage() { 14 | setContentRightHeader(); 15 | let $newEpisodesEntry = $('#menu-episodes'); 16 | let title = $newEpisodesEntry.find('span').html(); 17 | 18 | setHeader(generateHtmlTitle(title), ''); 19 | selectMenuItem($newEpisodesEntry); 20 | 21 | clearBody(); 22 | setScrollPositionOnTop(); 23 | 24 | setGridLayout(false); 25 | 26 | allNewEpisodes.ui.showAll(); 27 | } 28 | 29 | function showFavoritesPage() { 30 | setContentRightHeader(); 31 | let $favoritesEntry = $('#menu-favorites'); 32 | let title = $favoritesEntry.find('span').html(); 33 | 34 | setHeader(generateHtmlTitle(title)); 35 | selectMenuItem($favoritesEntry); 36 | setHeaderViewAction("list"); 37 | 38 | clearBody(); 39 | setScrollPositionOnTop(); 40 | 41 | let JsonContent = allFavoritePodcasts.getAll(); 42 | 43 | setGridLayout(true); 44 | 45 | if (allFavoritePodcasts.isEmpty()) 46 | allFavoritePodcasts.ui.showNothingToShowPage(); 47 | 48 | let List = document.getElementById("list"); 49 | for (let i in JsonContent) { 50 | let Artwork = getBestArtworkUrl(JsonContent[i].feedUrl); 51 | 52 | let ListElement = getPodcastElement(Artwork, JsonContent[i].data.collectionName); 53 | 54 | let HeaderElement = ListElement.getElementsByClassName('podcast-entry-header')[0] 55 | 56 | HeaderElement.getElementsByTagName("img")[0].setAttribute("draggable", false) 57 | 58 | $(ListElement).data(JsonContent[i]); 59 | $(HeaderElement).attr("feedUrl", JsonContent[i].feedUrl); 60 | 61 | ListElement.onclick = function (e) { 62 | if($(e.target).is('svg') || $(e.target).is('path') || $(e.target).hasClass('podcast-entry-actions') || $(e.target).hasClass('list-item-text')) { 63 | e.preventDefault(); 64 | return; 65 | } 66 | showAllEpisodes(this); 67 | } 68 | 69 | let $heartButton = $(ListElement).find('.podcast-entry-actions'); 70 | $heartButton.click(function () { 71 | $(this).stop(); 72 | unsubscribeListElement(this); 73 | }); 74 | 75 | $heartButton.hoverIntent(function () { 76 | setHeartContent($(this).find('svg'), true); 77 | }, function () { 78 | setHeartContent($(this).find('svg'), false); 79 | }); 80 | 81 | $(ListElement).mouseleave(function () { 82 | setHeartContent($(this).find('svg'), false); 83 | }) 84 | 85 | List.append(ListElement) 86 | } 87 | } 88 | 89 | function showArchivePage() { 90 | setContentRightHeader(); 91 | let $archiveEntry = $('#menu-archive'); 92 | let title = $archiveEntry.find('span').html(); 93 | 94 | setHeader(generateHtmlTitle(title), ''); 95 | selectMenuItem($archiveEntry); 96 | 97 | clearBody(); 98 | setScrollPositionOnTop(); 99 | 100 | setGridLayout(false); 101 | 102 | allArchiveEpisodes.ui.showAll() 103 | } 104 | 105 | function showStatisticsPage() { 106 | setContentRightHeader(); 107 | let $statisticsEntry = $('#menu-statistics'); 108 | let title = $statisticsEntry.find('span').html(); 109 | 110 | setHeader(generateHtmlTitle(title), ''); 111 | selectMenuItem($statisticsEntry); 112 | 113 | clearBody(); 114 | setScrollPositionOnTop(); 115 | 116 | setGridLayout(false); 117 | 118 | let list = document.getElementById("list"); 119 | 120 | list.append(getStatisticsHeaderElement("Podcasts")); 121 | 122 | list.append(getStatisticsEntryElement(i18n.__("Favorite Podcasts"), allFavoritePodcasts.length())); 123 | 124 | if(!allNewEpisodes.isEmpty()) { 125 | let channelName = allFavoritePodcasts.getByFeedUrl(allNewEpisodes.get(0).feedUrl).data.collectionName; 126 | list.append(getStatisticsEntryElement(i18n.__("Last Podcast"), channelName)); 127 | } else 128 | list.append(getStatisticsEntryElement(i18n.__("Last Podcast"), "None")); 129 | 130 | list.append(getStatisticsHeaderElement(i18n.__("Episodes"))); 131 | 132 | list.append(getStatisticsEntryElement(i18n.__("Archive Items"), allArchiveEpisodes.length())); 133 | 134 | list.append(getStatisticsEntryElement(i18n.__("New Episodes"), allNewEpisodes.length())); 135 | } 136 | 137 | function showPage(page) { 138 | if(allPreferences.ui.isOpen) { 139 | if(page == 'settings') 140 | return; 141 | 142 | allPreferences.ui.exitSettingsUI(); 143 | } 144 | 145 | switch(page) { 146 | case 'newEpisodes': 147 | showNewEpisodesPage(); 148 | break; 149 | case 'favorites': 150 | showFavoritesPage(); 151 | break; 152 | case 'archive': 153 | showArchivePage(); 154 | break; 155 | case 'statistics': 156 | showStatisticsPage(); 157 | break; 158 | case 'settings': 159 | allPreferences.ui.openSettingsUI(); 160 | break; 161 | case 'search': 162 | focusTextField("search-input"); 163 | break; 164 | default: 165 | 166 | break; 167 | } 168 | } -------------------------------------------------------------------------------- /app/js/new_episodes_class.js: -------------------------------------------------------------------------------- 1 | var allNewEpisodes = null; 2 | 3 | class NewEpisodesUI extends ListUI { 4 | 5 | showNothingToShow() { 6 | if(this.isNewEpisodesPage()) 7 | super.showNothingToShow(s_NewEpisodesNothingFoundIcon, 'new_episodes-nothing-to-show'); 8 | } 9 | 10 | isNewEpisodesPage() { 11 | return (this.getPageType() == 'newEpisodes'); 12 | } 13 | 14 | add(episode, i) { 15 | if(this.isNewEpisodesPage()) 16 | super.add(episode, i); 17 | 18 | setItemCounts(); 19 | } 20 | 21 | 22 | directAdd(episode, i, forceOriginalDirectAdd) { 23 | if(this.isNewEpisodesPage() || forceOriginalDirectAdd) 24 | super.directAdd(episode, i); 25 | } 26 | 27 | removeByEpisodeUrl(episodeUrl) { 28 | if(this.isNewEpisodesPage()) 29 | super.removeByEpisodeUrl(episodeUrl, this.dataObject.getAll()); 30 | 31 | setItemCounts(); 32 | } 33 | 34 | showAll() { 35 | if(this.dataObject.isEmpty()) 36 | this.showNothingToShow(); 37 | 38 | let $list = this.getList(); 39 | let epShownCounter = 0; 40 | for(let i in this.dataObject.episodes) { 41 | let episode = this.dataObject.get(i); 42 | if(!checkDateIsInTheLastWeek(episode)) 43 | this.dataObject.removeByEpisodeUrl(episode.episodeUrl); 44 | else if(epShownCounter < this.bufferSize) { 45 | $list.append(this.getNewItemList(episode)); 46 | epShownCounter++; 47 | } 48 | } 49 | setItemCounts(); 50 | 51 | let length = this.length() 52 | 53 | this.firstEpisodeDisplayed = 0; 54 | this.lastEpisodeDisplayed = length - 1; 55 | 56 | this.appendShowMoreEpisodesButton(); 57 | this.prependShowMoreEpisodesButton(); 58 | /* 59 | if(this.bufferSize < this.dataObject.length()) 60 | this.getShowMoreEpisodesBottomElement().show(); 61 | */ 62 | setScrollPositionOnTop(); 63 | } 64 | 65 | convertItemIntoInfoItemList(obj) { 66 | let episode = _(obj); 67 | 68 | let $obj = $(obj); 69 | $obj.attr('info-mode', ''); 70 | let $descriptionItem = $obj.find('.list-item-description'); 71 | $obj.off('click'); 72 | 73 | $obj.find('div').not(".list-item-description").css('display', 'none'); 74 | $obj.css('grid-template-columns', '5em 1fr 5em 5em'); 75 | $descriptionItem.before( 76 | ` 77 |
    78 | 79 | ${episode.episodeTitle} 80 | 81 |
    82 | 83 | ${episode.channelName} 84 | 85 |
    86 | 87 | ${$obj.find('.list-item-sub-text').html()} 88 | 89 |
    90 | 91 | ${getInfoFromDescription(episode.episodeDescription)} 92 | 93 |
    94 | 95 | ${new Date(episode.pubDate).toLocaleString()} 96 | 97 | 98 | ${allArchiveEpisodes.ui.getDownloadStateButton(episode.episodeUrl)} 99 | 100 |
    101 |
    102 |
    ` 103 | ) 104 | 105 | $obj.find('#info-item-list') 106 | .stop() 107 | .animate({opacity: 1.0}, 500); 108 | 109 | $descriptionItem.html(s_ArrowUpIcon); 110 | 111 | let initialHeight = $obj.css('height'); 112 | $obj.css('height', 'auto'); 113 | let autoHeight = $obj.css('height'); 114 | $obj.css('height', initialHeight) 115 | 116 | $obj.find('img') 117 | .css('position', 'relative') 118 | .css('top', '0px') 119 | .stop() 120 | .animate({top: '22px'}, 300); 121 | 122 | $obj 123 | .stop() 124 | .animate( 125 | {height: autoHeight}, 126 | 300, 127 | function () { 128 | $obj.css('height', 'auto'); 129 | }); 130 | } 131 | 132 | convertInfoItemIntoItemList($obj) { 133 | if($obj.get(0)) { 134 | let height = $obj.get(0).offsetHeight; 135 | $obj.removeAttr('info-mode'); 136 | 137 | $obj.click(function(e) { 138 | if($(e.target).is('svg') || $(e.target).is('path') || $(e.target).hasClass('list-item-icon') || $(e.target).hasClass('list-item-text')) { 139 | e.preventDefault(); 140 | return; 141 | } 142 | playerManager.startsPlaying(_(this)); 143 | }); 144 | 145 | $obj.find('div') 146 | .not('.list-item-flag') 147 | .removeAttr('style'); 148 | $obj.css('grid-template-columns', '5em 1fr 6em 1fr 6em 5em 5em'); 149 | 150 | $obj.find('img') 151 | .stop() 152 | .animate({top: '0px'}, 300, function () { 153 | $obj.find('img').removeAttr('style'); 154 | }); 155 | 156 | $obj 157 | .css('background-color', '') 158 | .css('height', height) 159 | .stop() 160 | .animate( 161 | {height: '3.2em'}, //2.86em 162 | 300, 163 | function () { 164 | $obj.css('height', ''); 165 | }); 166 | 167 | 168 | $obj.find('#info-item-list').remove(); 169 | 170 | $obj.find('.list-item-description') 171 | .html(s_InfoIcon); 172 | 173 | 174 | $obj.find('.list-item-flag') 175 | .css('display', ''); 176 | } 177 | } 178 | 179 | getNewItemList(newEpisode) { 180 | let episode = getInfoEpisodeByObj(newEpisode); 181 | 182 | let artwork = episode.artwork; 183 | let duration = getDurationFromDurationKey(episode); 184 | 185 | let $listElement = $(buildListItem( 186 | [ 187 | getImagePart(artwork), 188 | getBoldTextPart(episode.episodeTitle), 189 | getSubTextPart(duration), 190 | getTextPart(episode.channelName), 191 | getProgressionFlagPart(episode.episodeUrl), 192 | getDescriptionPart(), 193 | getAddToArchiveButtonPart(episode.episodeUrl) 194 | ], 195 | "5em 1fr 6em 1fr 6em 5em 5em" 196 | )); 197 | 198 | $listElement.click(function(e) { 199 | if($(e.target).is('svg') || $(e.target).is('path') || $(e.target).hasClass('list-item-icon') || $(e.target).hasClass('list-item-text')) { 200 | e.preventDefault(); 201 | return; 202 | } 203 | playerManager.startsPlaying(_(this)); 204 | }); 205 | 206 | $listElement.data(episode); 207 | $listElement.attr('url', episode.episodeUrl); 208 | 209 | if(allArchiveEpisodes.downloadManager.isDownloadInProgress(episode.episodeUrl)) 210 | $listElement 211 | .css('--progress', `${allArchiveEpisodes.downloadManager.data[episode.episodeUrl].progress || 0}%`) 212 | .addClass("download-in-progress"); 213 | 214 | switch(allArchiveEpisodes.getStateDownload(episode.episodeUrl)) { 215 | case 'in_progress': 216 | $listElement.addClass("download-in-progress"); 217 | break; 218 | case 'error': 219 | $listElement.addClass("download-error"); 220 | break; 221 | default: 222 | break; 223 | } 224 | 225 | if(playerManager.isPlaying(episode.episodeUrl)) 226 | $listElement.addClass("select-episode") 227 | 228 | $listElement.find('.list-item-description').click(() => { 229 | if($listElement.is('[info-mode]')) 230 | this.convertInfoItemIntoItemList($listElement); 231 | else { 232 | this.convertInfoItemIntoItemList(this.getAllItemsList().filter('[info-mode]')); 233 | this.convertItemIntoInfoItemList($listElement); 234 | } 235 | }) 236 | 237 | return $listElement; 238 | } 239 | } 240 | 241 | class NewEpisodes { 242 | constructor() { 243 | this.load(); 244 | this.ui = new NewEpisodesUI(this); 245 | } 246 | 247 | load() { 248 | if (!fs.existsSync(getNewEpisodesSaveFilePath())) 249 | fs.openSync(getNewEpisodesSaveFilePath(), 'w'); 250 | 251 | let fileContent = ifExistsReadFile(getNewEpisodesSaveFilePath()); 252 | this.episodes = JSON.parse(fileContent == "" ? "[]": fileContent); 253 | } 254 | 255 | update() { 256 | fs.writeFileSync(getNewEpisodesSaveFilePath(), JSON.stringify(this.episodes, null, "\t")); 257 | } 258 | 259 | length() { 260 | return this.episodes.length; 261 | } 262 | 263 | isEmpty() { 264 | return (this.length() == 0); 265 | } 266 | 267 | getAll() { 268 | return this.episodes; 269 | } 270 | 271 | get(i) { 272 | return this.episodes[i]; 273 | } 274 | 275 | getInfoByIndex(i) { 276 | let newEpisode = this.get(i); 277 | return getInfoEpisodeByObj(newEpisode); 278 | } 279 | 280 | findByEpisodeUrl(episodeUrl) { 281 | for(let i in this.episodes) 282 | if(this.episodes[i].episodeUrl == episodeUrl) 283 | return i; 284 | return -1; 285 | } 286 | 287 | getByEpisodeUrl(episodeUrl) { 288 | let i = this.findByEpisodeUrl(episodeUrl); 289 | return (i != -1 ? this.episodes[i] : undefined); 290 | } 291 | 292 | add(episode) { 293 | episode = { 294 | feedUrl: episode.feedUrl, 295 | episodeUrl: episode.episodeUrl 296 | } 297 | if(this.findByEpisodeUrl(episode.episodeUrl) == -1) { 298 | let i = 0; 299 | while(i < this.length() && compareEpisodeDates(this.episodes[i], episode) > 0) 300 | i++; 301 | this.episodes.splice(i, 0, episode); 302 | this.update(); 303 | this.ui.add(episode, i); 304 | return episode; 305 | } 306 | return null; 307 | } 308 | 309 | removeByEpisodeUrl(episodeUrl) { 310 | let i = this.findByEpisodeUrl(episodeUrl); 311 | if(i != -1) { 312 | this.episodes.splice(i, 1); 313 | this.update(); 314 | this.ui.removeByEpisodeUrl(episodeUrl); 315 | return true; 316 | } 317 | return false; 318 | } 319 | 320 | removePodcastEpisodes(feedUrl) { 321 | for(let i = this.episodes.length - 1; i >= 0; i--) { 322 | if(this.episodes[i].feedUrl == feedUrl) 323 | this.episodes.splice(i, 1); 324 | } 325 | this.update(); 326 | } 327 | } 328 | 329 | function loadNewEpisodes() { 330 | allNewEpisodes = new NewEpisodes(); 331 | } -------------------------------------------------------------------------------- /app/js/pages.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Body 4 | */ 5 | 6 | function clearBody() { 7 | $('#content-right #content-right-body #list').html(''); 8 | } 9 | 10 | function setBody(bodyHtml) { 11 | $('#content-right #content-right-body #list').html(bodyHtml); 12 | } 13 | 14 | function getBody() { 15 | return $('#content-right #content-right-body #list').html(); 16 | } 17 | 18 | function setNothingToShowBody(icon, id, warning) { 19 | if(!id || !$('#' + id).get(0)) { 20 | id = !id ? '' : id; 21 | 22 | setGridLayout(false); 23 | let $body = 24 | ` 25 |
    26 | ${icon} 27 |

    28 | ${i18n.__('Nothing to show')} 29 | ${(warning ? 30 | `

    31 | 32 | ${i18n.__(warning)} 33 | ` : '')} 34 |
    `; 35 | 36 | if(id === 'feed-nothing-to-show') 37 | allFeeds.ui.getHeader().after($body); 38 | else 39 | setBody($body); 40 | } 41 | } 42 | 43 | /* 44 | * Header 45 | */ 46 | 47 | function clearHeader() { 48 | $('#content-right #content-right-header h1').html(''); 49 | } 50 | 51 | function setHeader(headerHtml, buttonHtml) { 52 | if(headerHtml != undefined) 53 | $('#content-right #content-right-header h1').html(headerHtml); 54 | 55 | if(buttonHtml != undefined) 56 | $('#content-right #content-right-header div').html(buttonHtml); 57 | } 58 | 59 | function getHeader() { 60 | return $('#content-right #content-right-header h1').html(); 61 | } 62 | 63 | function generateHtmlTitle(title) { 64 | return '' + i18n.__(title) + '' 65 | } 66 | 67 | /* 68 | * Page 69 | */ 70 | /* 71 | function showPage(headerHtml, bodyHtml) { 72 | setHeader(headerHtml) 73 | setBody(bodyHtml) 74 | } */ 75 | 76 | function setScrollPositionOnTop() { 77 | $('#content-right').scrollTop(0); 78 | } 79 | 80 | function removeContentRightHeader() { 81 | $('#content-right').addClass('header-null'); 82 | } 83 | 84 | function setContentRightHeader() { 85 | $('#content-right').removeClass('header-null'); 86 | } -------------------------------------------------------------------------------- /app/js/playback_class.js: -------------------------------------------------------------------------------- 1 | class PlaybackUI extends UI { 2 | constructor(obj) { 3 | super(); 4 | this.dataObject = obj; 5 | } 6 | 7 | update(episodeUrl) { 8 | this.updateDone(episodeUrl); 9 | } 10 | 11 | updateDone(episodeUrl) { 12 | this.getByEpisodeUrl(episodeUrl).find('.list-item-flag') 13 | .css('--percentage', '100%') 14 | .css('--flag-color-1', '119, 153, 136') 15 | .html('Done'); 16 | } 17 | 18 | getProgressionFlag(episodeUrl) { 19 | let done = this.dataObject.getDone(episodeUrl); 20 | if(done) 21 | return getFlagPart('Done') 22 | .css('--percentage', '100%') 23 | .css('--flag-color-1', '119, 153, 136') 24 | .get(0); 25 | 26 | let duration = this.dataObject.getDuration(episodeUrl); 27 | if(duration == null || duration == undefined || duration < 0) 28 | return getFlagPart('0%').css('--percentage', '0%').get(0); 29 | 30 | let position = this.dataObject.getPosition(episodeUrl); 31 | let percentage = getPercentage(position, duration) + '%'; 32 | return getFlagPart(percentage).css('--percentage', percentage).get(0); 33 | } 34 | 35 | updatePosition(episodeUrl, position) { 36 | if(!Number.isNaN(position) && !this.dataObject.getDone(episodeUrl)) { 37 | let percentage = position + '%'; 38 | this.getByEpisodeUrl(episodeUrl).find('.list-item-flag') 39 | .css('--percentage', percentage) 40 | .html(percentage); 41 | } 42 | } 43 | 44 | } 45 | 46 | class Playback { 47 | constructor() { 48 | this.load(); 49 | this.ui = new PlaybackUI(this); 50 | this.bufferSize = -1000; 51 | } 52 | 53 | load() { 54 | if (!fs.existsSync(getPlaybackSaveFilePath())) 55 | fs.openSync(getPlaybackSaveFilePath(), 'w'); 56 | 57 | let fileContent = ifExistsReadFile(getPlaybackSaveFilePath()); 58 | this.data = JSON.parse(fileContent == "" ? "{}": fileContent); 59 | } 60 | 61 | update() { 62 | fs.writeFileSync(getPlaybackSaveFilePath(), JSON.stringify(this.data, null, "\t")); 63 | } 64 | 65 | exists(episodeUrl) { 66 | return Boolean(this.data[episodeUrl]); 67 | } 68 | 69 | length() { 70 | Object.keys(this.data).length; 71 | } 72 | 73 | add(feedUrl, episodeUrl) { 74 | if(!this.exists(episodeUrl)) { 75 | this.unsafeAdd(feedUrl, episodeUrl); 76 | this.update(); 77 | return true; 78 | } 79 | return false; 80 | } 81 | 82 | unsafeAdd(feedUrl, episodeUrl) { 83 | if(!this.exists(episodeUrl)) { 84 | this.data[episodeUrl] = { 85 | feedUrl: feedUrl, 86 | position: 0, 87 | duration: -1, 88 | done: false 89 | }; 90 | if(this.length() > this.bufferSize) // limitation canceled, buffer size is a negative number 91 | this.unsafeRemoveOlder(); 92 | return true; 93 | } 94 | return false; 95 | } 96 | 97 | get(episodeUrl) { 98 | return this.data[episodeUrl]; 99 | } 100 | 101 | getPosition(episodeUrl) { 102 | let playback = this.get(episodeUrl); 103 | if(!playback) 104 | return 0; 105 | return playback.position; 106 | } 107 | 108 | getDuration(episodeUrl) { 109 | let playback = this.get(episodeUrl); 110 | if(!playback) 111 | return -1; 112 | return playback.duration; 113 | } 114 | 115 | getDone(episodeUrl) { 116 | let playback = this.get(episodeUrl); 117 | if(!playback) 118 | return false; 119 | return Boolean(playback.done); 120 | } 121 | 122 | alwaysGet(feedUrl, episodeUrl) { 123 | if(!this.exists(episodeUrl)) 124 | this.add(feedUrl, episodeUrl) 125 | return this.data[episodeUrl]; 126 | } 127 | 128 | unsafeSet(episodeUrl, obj) { 129 | if(this.exists(episodeUrl)) { 130 | this.data[episodeUrl] = obj; 131 | return true; 132 | } 133 | return false; 134 | } 135 | 136 | set(episodeUrl, obj) { 137 | if(this.unsafeSet(episodeUrl, obj)) { 138 | this.update(); 139 | this.ui.update(episodeUrl); 140 | return true; 141 | } 142 | return false; 143 | } 144 | 145 | setPosition(episodeUrl, position) { 146 | if(this.exists(episodeUrl)) { 147 | this.data[episodeUrl].position = position; 148 | this.update(); 149 | return true; 150 | } 151 | return false; 152 | } 153 | 154 | setDuration(episodeUrl, duration) { 155 | if(this.exists(episodeUrl)) { 156 | this.data[episodeUrl].duration = duration; 157 | this.update(); 158 | return true; 159 | } 160 | return false; 161 | } 162 | 163 | setDone(episodeUrl, done) { 164 | if(this.exists(episodeUrl)) { 165 | this.data[episodeUrl].done = done; 166 | this.update(); 167 | this.ui.updateDone(episodeUrl); 168 | return true; 169 | } 170 | return false; 171 | } 172 | 173 | alwaysSet(episodeUrl, obj) { 174 | this.data[episodeUrl] = obj; 175 | this.update(); 176 | this.ui.update(episodeUrl); 177 | } 178 | 179 | alwaysSetPositionAndDuration(feedUrl, episodeUrl, position, duration) { 180 | if(!this.exists(episodeUrl)) 181 | this.unsafeAdd(feedUrl, episodeUrl) 182 | this.data[episodeUrl].position = position; 183 | if(!this.data[episodeUrl].duration || duration) 184 | this.data[episodeUrl].duration = duration; 185 | this.update(); 186 | this.ui.updatePosition(episodeUrl, getPercentage(position, duration)); 187 | } 188 | 189 | alwaysSetDone(feedUrl, episodeUrl, done) { 190 | if(!this.exists(episodeUrl)) 191 | this.unsafeAdd(feedUrl, episodeUrl) 192 | this.data[episodeUrl].done = done; 193 | this.update(); 194 | this.ui.updateDone(episodeUrl); 195 | } 196 | 197 | remove(episodeUrl) { 198 | if(this.exists(episodeUrl)) { 199 | this.unsafeRemove(episodeUrl); 200 | this.update(); 201 | return true; 202 | } 203 | return false; 204 | } 205 | 206 | removeOlder() { 207 | let episodeUrl = Object.keys(this.data)[0]; 208 | return this.remove(episodeUrl); 209 | } 210 | 211 | unsafeRemove(episodeUrl) { 212 | if(this.exists(episodeUrl)) { 213 | delete this.data[episodeUrl]; 214 | return true; 215 | } 216 | return false; 217 | } 218 | 219 | unsafeRemoveOlder() { 220 | let episodeUrl = Object.keys(this.data)[0]; 221 | return this.unsafeRemove(episodeUrl); 222 | } 223 | 224 | removeAllDataPodcast(feedUrl) { 225 | let updated = false; 226 | for(let episodeUrl in this.data) 227 | if(this.data[episodeUrl].feedUrl == feedUrl) { 228 | this.unsafeRemove(episodeUrl); 229 | updated = true; 230 | } 231 | if(updated) 232 | this.update(); 233 | } 234 | } 235 | 236 | function getPercentage(position, duration) { 237 | return Math.floor((position / duration) * 100); 238 | } -------------------------------------------------------------------------------- /app/js/podcast_class.js: -------------------------------------------------------------------------------- 1 | class Podcast { 2 | constructor(artistName, collectionName, artworkUrl, feedUrl, description) { 3 | this.feedUrl = feedUrl; 4 | 5 | this.data = { 6 | artistName: artistName, 7 | collectionName: collectionName, 8 | artworkUrl: artworkUrl, 9 | description: (description ? description : '') 10 | }; 11 | 12 | this.excludeFromNewEpisodes = false; 13 | } 14 | 15 | isValid() { 16 | return (this.data.collectionName && this.feedUrl); 17 | } 18 | } -------------------------------------------------------------------------------- /app/js/request.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | const progress = require('request-progress'); 3 | 4 | function makeRequest(url, callback, error) { 5 | $.ajax({ 6 | url: url, 7 | method: "GET", 8 | dataType: "text", 9 | cache: false, 10 | success: (data) => { 11 | callback(data, url); 12 | }, 13 | error: (e) => { 14 | if(error) 15 | error(e); 16 | } 17 | }); 18 | } 19 | 20 | function downloadFile(url, path, error, end, _progress) { 21 | let stream = fs.createWriteStream(path); 22 | let _request = request({ 23 | url: url, 24 | method: 'GET', 25 | agentOptions: { 26 | rejectUnauthorized: false, 27 | timeout: 5000 28 | }}/* , (e) => { 29 | if(!(e instanceof Error)) 30 | success(); 31 | } */) 32 | progress(_request).on('progress', (state) => { 33 | //console.log(state); 34 | if(_progress) 35 | _progress(state); 36 | /* 37 | { 38 | percentage: 0.5, // Overall percentage (between 0 to 1) 39 | speed: 554732, // The download speed in bytes/sec 40 | size: { 41 | total: 90044871, // The total payload size in bytes 42 | transferred: 27610959 // The transferred payload size in bytes 43 | }, 44 | time: { 45 | elapsed: 36.235, // The total elapsed seconds since the start (3 decimals) 46 | remaining: 81.403 // The remaining seconds to finish (3 decimals) 47 | } 48 | } 49 | */ 50 | 51 | }).on('error', function (e) { 52 | if(error) 53 | error(e); 54 | }).on('end', () => { 55 | if(end) 56 | end(); 57 | }).pipe(stream); 58 | 59 | let download = { 60 | 'stream': stream, 61 | 'request': _request 62 | } 63 | 64 | return download; 65 | } 66 | -------------------------------------------------------------------------------- /app/js/search.js: -------------------------------------------------------------------------------- 1 | var searchTimeoutVar = null; 2 | 3 | function search(self, event) { 4 | if (event.code == "Escape") 5 | clearTextField(self); 6 | else { 7 | if(!$('#search-nothing-to-show').get(0)) 8 | clearBody(); 9 | 10 | setContentRightHeader(); 11 | clearMenuSelection(); 12 | setHeader(generateHtmlTitle("Search"), ''); 13 | 14 | $('#res').attr('return-value', ''); 15 | 16 | if(isUrl(self.value)) 17 | getPodcastInfoFromFeedUrl(self.value); 18 | else 19 | getPodcasts(self.value); 20 | } 21 | } 22 | 23 | function isUrl(str) { 24 | let url = str.match(/\b(https?:\/\/\S*\b)/g); 25 | return (url && url[0] == str); 26 | } 27 | 28 | function getPodcastInfoFromFeedUrl(feedUrl) { 29 | makeRequest(feedUrl, (xml) => getPodcastInfoFromXml(xml, feedUrl), () => getPodcasts(feedUrl)); 30 | } 31 | 32 | function getPodcastInfoFromXml(xml, feedUrl) { 33 | let parser = new DOMParser(); 34 | let xmlDoc = parser.parseFromString(xml, "text/xml"); 35 | 36 | let channel = xmlDoc.getElementsByTagName("channel")[0]; 37 | if(!channel) { 38 | showSearchNothingToShow(); 39 | return; 40 | } 41 | 42 | let channelName = channel.getElementsByTagName("title")[0].childNodes[0].nodeValue; 43 | 44 | let artworkUrl = channel.getElementsByTagName("itunes:image")[0]; 45 | if(artworkUrl) 46 | artworkUrl = artworkUrl.getAttribute('href'); 47 | else { 48 | artworkUrl = channel.getElementsByTagName("image")[0]; 49 | if(artworkUrl) { 50 | artworkUrl = artworkUrl.getElementsByTagName("url")[0]; 51 | if(artworkUrl) 52 | artworkUrl = artworkUrl.textContent; 53 | } 54 | } 55 | 56 | let artistName = channel.getElementsByTagName("itunes:author")[0]; 57 | if(artistName) 58 | artistName = artistName.textContent; 59 | else { 60 | let artistName = channel.getElementsByTagName("author")[0]; 61 | if(artistName) 62 | artistName = artistName.childNodes[0].nodeValue; 63 | else 64 | artistName = ''; 65 | } 66 | 67 | let podcastDescription = ''; 68 | let podcastSubtitle = channel.getElementsByTagName('itunes:subtitle')[0]; 69 | if(podcastSubtitle) 70 | podcastDescription = podcastSubtitle.textContent; 71 | podcastSubtitle = channel.getElementsByTagName('description')[0]; 72 | if(podcastSubtitle && podcastSubtitle.textContent.length > podcastDescription.length) 73 | podcastDescription = podcastSubtitle.textContent; 74 | 75 | podcastSubtitle = channel.getElementsByTagName('itunes:summary')[0]; 76 | if(podcastSubtitle && podcastSubtitle.textContent.length > podcastDescription.length) 77 | podcastDescription = podcastSubtitle.textContent; 78 | 79 | showSearchList([{ 80 | artworkUrl60: artworkUrl, 81 | collectionName: channelName, 82 | artistName: artistName, 83 | feedUrl: feedUrl 84 | }]) 85 | } 86 | 87 | function getPodcasts(searchTerm) { 88 | if(searchTimeoutVar) 89 | clearTimeout(searchTimeoutVar); 90 | 91 | if(!searchTerm) { 92 | showSearchNothingToShow(); 93 | return; 94 | } 95 | 96 | searchTimeoutVar = setTimeout(() => { 97 | searchTerm = encodeURIComponent(searchTerm); 98 | makeRequest(getITunesSearchUrl(searchTerm), getResults); 99 | }, 300); 100 | } 101 | 102 | function getResults(data, feedUrl) { 103 | let query = decodeURIComponent(feedUrl).split('=')[1].split('&')[0]; 104 | 105 | let obj = JSON.parse(data); 106 | 107 | if(obj.results.length == 0) 108 | showSearchNothingToShow(); 109 | else if(query == $('#search-input').val()) { 110 | $('#content-right-header span').data(obj); 111 | showSearchList(obj.results); 112 | } 113 | } 114 | 115 | function showSearchList(results) { 116 | setContentRightHeader(); 117 | clearBody(); 118 | setGridLayout(false); 119 | 120 | let $list = $('#list'); 121 | for (let i in results) { 122 | let Artwork = results[i].artworkUrl100; 123 | if(Artwork == undefined || Artwork == 'undefined') { 124 | Artwork = results[i].artworkUrl60; 125 | if(Artwork == undefined || Artwork == 'undefined') 126 | Artwork = getGenericArtwork(); 127 | } 128 | let podcast = new Podcast ( 129 | results[i].artistName, 130 | results[i].collectionName, 131 | Artwork, 132 | results[i].feedUrl 133 | ); 134 | 135 | var HeartButton = null; 136 | if (isAlreadyFavorite(podcast.feedUrl)) 137 | HeartButton = getFullHeartButton(podcast); 138 | else 139 | HeartButton = getHeartButton(podcast); 140 | 141 | let item = buildListItem( 142 | [ 143 | getImagePart(results[i].artworkUrl60), 144 | getBoldTextPart(podcast.data.collectionName), 145 | getSubTextPart(podcast.data.artistName), 146 | HeartButton 147 | ], 148 | "5em 1fr 1fr 5em" 149 | ); 150 | 151 | $(item).data(podcast); 152 | item.onclick = function (e) { 153 | if($(e.target).is('svg') || $(e.target).is('path') || $(e.target).hasClass('list-item-icon') || $(e.target).hasClass('list-item-text')) { 154 | e.preventDefault(); 155 | return; 156 | } 157 | showAllEpisodes(this); 158 | } 159 | $list.append(item); 160 | 161 | } 162 | } 163 | 164 | function isSearchPage() { 165 | return getHeader() == generateHtmlTitle("Search"); 166 | } 167 | 168 | function showSearchNothingToShow() { 169 | if(isSearchPage()) 170 | setNothingToShowBody(s_SearchNothingFoundIcon, 'search-nothing-to-show', 'Try typing the feed url into the search field!'); 171 | } 172 | 173 | function getHeartButton(podcastInfos) { 174 | let $heartButtonElement = $(getIconButtonPart(s_Heart)); 175 | 176 | $heartButtonElement.find('svg').click(function () { 177 | setFavorite(this, podcastInfos.data.artistName, 178 | podcastInfos.data.collectionName, 179 | podcastInfos.data.artworkUrl, 180 | podcastInfos.feedUrl, 181 | podcastInfos.data.description 182 | ); 183 | }); 184 | return $heartButtonElement.get(0); 185 | } 186 | 187 | function getFullHeartButton(podcastInfos) { 188 | let $heartButtonElement = $(getIconButtonPart(s_FullHeart)); 189 | 190 | $heartButtonElement.find('svg').click(function() { 191 | unsetFavorite(this, podcastInfos.data.artistName, 192 | podcastInfos.data.collectionName, 193 | podcastInfos.data.artworkUrl, 194 | podcastInfos.feedUrl, 195 | podcastInfos.data.description 196 | ); 197 | }); 198 | return $heartButtonElement.get(0); 199 | } 200 | 201 | function getITunesSearchUrl(searchTerm) { 202 | return 'http://itunes.apple.com/search?term=' + searchTerm + '&media=podcast'; 203 | } -------------------------------------------------------------------------------- /app/js/slider_class.js: -------------------------------------------------------------------------------- 1 | class Slider { 2 | constructor($slider) { 3 | this.$el = $($slider); 4 | this.value = 0; 5 | this.mouseup = true; 6 | 7 | this.disable(); 8 | this.init(); 9 | 10 | } 11 | 12 | init() { 13 | this.onchange(() => { 14 | this.update(); 15 | }); 16 | 17 | this.onmousedown(() => { 18 | this.mouseup = false; 19 | }); 20 | 21 | this.onmouseup(() => { 22 | this.mouseup = true; 23 | 24 | playerManager.setCurrentTime(this.value); 25 | }); 26 | 27 | } 28 | 29 | disable() { 30 | if(!this.disableState) { 31 | this.disableState = true; 32 | this.$el.prop( "disabled", this.disableState ); 33 | } 34 | } 35 | 36 | enable() { 37 | if(this.disableState) { 38 | this.disableState = false; 39 | this.$el.prop( "disabled", this.disableState ); 40 | } 41 | } 42 | 43 | update() { 44 | this.getValue(); 45 | this.updateProgress(); 46 | } 47 | 48 | updateProgress() { 49 | this.$el.css("--progress-slider", `${this.value}%`); 50 | } 51 | 52 | getValue() { 53 | this.value = this.$el.val(); 54 | return this.value; 55 | } 56 | 57 | setValue(value) { 58 | this.enable(); 59 | 60 | if(this.mouseup && !isNaN(value)) { 61 | this.$el.val(value); 62 | this.update(); 63 | } 64 | } 65 | 66 | onchange(f) { 67 | this.$el.on("input change", f); 68 | } 69 | 70 | onmouseup(f) { 71 | this.$el.mouseup(f); 72 | this.$el.bind('touchend', f); 73 | } 74 | 75 | onmousedown(f) { 76 | this.$el.mousedown(f); 77 | this.$el.bind('touchstart', f); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/js/translations.js: -------------------------------------------------------------------------------- 1 | 2 | function translate() { 3 | translateByDescriptor(".new-episodes", 'New Episodes'); 4 | translateByDescriptor(".favorites", 'Favorites'); 5 | translateByDescriptor(".archive", 'Archive'); 6 | translateByDescriptor(".settings", 'Settings'); 7 | translateByDescriptor(".statistics", 'Statistics'); 8 | 9 | translateByDescriptor('#content-left-player-title>div', 'No episode selected'); 10 | 11 | 12 | translateByDescriptor(".dark-mode-translate", 'Dark mode'); 13 | 14 | document.getElementsByName('search')[0].placeholder=i18n.__('Search'); 15 | } 16 | 17 | function translateByDescriptor(descriptor, value){ 18 | $(descriptor).html(i18n.__(value)); 19 | } 20 | -------------------------------------------------------------------------------- /app/js/ui_class.js: -------------------------------------------------------------------------------- 1 | class UI { 2 | constructor() { 3 | } 4 | 5 | /* 6 | * PAGE 7 | */ 8 | 9 | getPageType() { 10 | if(allFeeds.ui.getHeader().get(0)) 11 | return 'feed'; 12 | if(getHeader() == generateHtmlTitle('New Episodes')) 13 | return 'newEpisodes'; 14 | if((getHeader() == generateHtmlTitle('Favorites'))) 15 | return 'favorites'; 16 | if(getHeader() == generateHtmlTitle('Archive')) 17 | return 'archive'; 18 | return undefined; 19 | } 20 | 21 | /* 22 | * LIST 23 | */ 24 | 25 | length() { 26 | return this.getAllItemsList().length; 27 | } 28 | 29 | isEmpty() { 30 | return !Boolean(this.getAllItemsList().get(0)); 31 | } 32 | 33 | getList() { 34 | return $('#list'); 35 | } 36 | 37 | getAllItemsList() { 38 | return $('#list li'); 39 | } 40 | 41 | getFirstItemList() { 42 | return $(this.getAllItemsList().get(0)); 43 | } 44 | 45 | getLastItemList() { 46 | let $itemList = this.getAllItemsList(); 47 | return $($itemList.get($itemList.length - 1)); 48 | } 49 | 50 | getItemListByIndex(i) { 51 | return $(this.getAllItemsList().get(i)); 52 | } 53 | 54 | getByEpisodeUrl(episodeUrl) { 55 | return this.getAllItemsList().filter('[url="' + episodeUrl + '"]'); 56 | } 57 | } -------------------------------------------------------------------------------- /app/js/update_feed_worker.js: -------------------------------------------------------------------------------- 1 | function getDifferenceFeed(oldFeed, newFeed) { 2 | let feedUrl = oldFeed.length == 0 ? newFeed[0].feedUrl : oldFeed[0].feedUrl; 3 | let feed = newFeed; 4 | 5 | oldFeed = oldFeed.map(x => x.episodeUrl); 6 | newFeed = newFeed.map(x => x.episodeUrl); 7 | 8 | let new_episodes = newFeed.filter(x => oldFeed.indexOf(x) === -1); 9 | let deleted_episodes = oldFeed.filter(x => !newFeed.includes(x)); 10 | 11 | if(new_episodes.length == 0 && deleted_episodes.length == 0) 12 | return; 13 | 14 | postMessage({ 15 | feedUrl: feedUrl, 16 | new_episodes: new_episodes, 17 | deleted_episodes: deleted_episodes, 18 | initialLength: oldFeed.length, 19 | feed: feed 20 | }); 21 | } 22 | 23 | onmessage = function (ev) { 24 | try { 25 | getDifferenceFeed(ev.data.oldFeed, ev.data.newFeed); 26 | } catch(err) { 27 | console.log(err) 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /app/js/xmlparser_worker.js: -------------------------------------------------------------------------------- 1 | importScripts('./lib/jsdom.js') 2 | importScripts('./podcast_class.js') 3 | importScripts('./episode_class.js') 4 | 5 | function xmlParser(xml, feedUrl, artwork) { 6 | let xmlDoc = new JSDOM(xml, {contentType: "text/xml"}).window.document; 7 | 8 | let channel = xmlDoc.getElementsByTagName("channel")[0]; 9 | let channelName = channel.getElementsByTagName("title")[0].childNodes[0].nodeValue; 10 | 11 | let artworkUrl = channel.getElementsByTagName("itunes:image")[0]; 12 | if(artworkUrl) 13 | artworkUrl = artworkUrl.getAttribute('href'); 14 | else { 15 | artworkUrl = channel.getElementsByTagName("image")[0]; 16 | if(artworkUrl) { 17 | artworkUrl = artworkUrl.getElementsByTagName("url")[0]; 18 | if(artworkUrl) 19 | artworkUrl = artworkUrl.textContent; 20 | } 21 | } 22 | 23 | let artistName = channel.getElementsByTagName("itunes:author")[0]; 24 | if(artistName) 25 | artistName = artistName.textContent; 26 | else { 27 | let artistName = channel.getElementsByTagName("author")[0]; 28 | if(artistName) 29 | artistName = artistName.childNodes[0].nodeValue; 30 | else 31 | artistName = ''; 32 | } 33 | 34 | let podcastDescription = ''; 35 | let podcastSubtitle = channel.getElementsByTagName('itunes:subtitle')[0]; 36 | if(podcastSubtitle) 37 | podcastDescription = podcastSubtitle.textContent; 38 | podcastSubtitle = channel.getElementsByTagName('description')[0]; 39 | if(podcastSubtitle && podcastSubtitle.textContent.length > podcastDescription.length) 40 | podcastDescription = podcastSubtitle.textContent; 41 | 42 | podcastSubtitle = channel.getElementsByTagName('itunes:summary')[0]; 43 | if(podcastSubtitle && podcastSubtitle.textContent.length > podcastDescription.length) 44 | podcastDescription = podcastSubtitle.textContent; 45 | 46 | let podcastData = new Podcast( 47 | artistName, 48 | channelName, 49 | artworkUrl, 50 | feedUrl, 51 | podcastDescription 52 | ); 53 | 54 | let json = []; 55 | let items = xmlDoc.getElementsByTagName("item"); 56 | for(let i = 0; i < items.length; i++) { 57 | let item = items[i]; 58 | let enclosure = item.getElementsByTagName('enclosure')[0]; 59 | 60 | let description = ''; 61 | let subtitle = item.getElementsByTagName('itunes:subtitle')[0]; 62 | if(subtitle) 63 | description = subtitle.textContent; 64 | 65 | subtitle = item.getElementsByTagName('description')[0]; 66 | if(subtitle && subtitle.textContent.length > description.length) 67 | description = subtitle.textContent; 68 | 69 | subtitle = item.getElementsByTagName('itunes:summary')[0]; 70 | if(subtitle && subtitle.textContent.length > description.length) 71 | description = subtitle.textContent; 72 | 73 | let duration = item.getElementsByTagName('itunes:duration')[0]; 74 | if(duration) 75 | duration = duration.innerHTML; 76 | else { 77 | duration = item.getElementsByTagName('duration')[0]; 78 | if(duration) 79 | duration = duration.innerHTML; 80 | else 81 | duration = ''; 82 | } 83 | 84 | let episode = new Episode ( 85 | channelName, 86 | feedUrl, 87 | item.getElementsByTagName("title")[0].childNodes[0].nodeValue, 88 | (enclosure ? enclosure.getAttribute('url').split("?")[0] : ''), 89 | (enclosure ? enclosure.getAttribute('type') : ''), 90 | (enclosure ? enclosure.getAttribute('length') : ''), 91 | description, 92 | duration, 93 | item.getElementsByTagName('pubDate')[0].innerHTML, 94 | artwork 95 | ); 96 | json.push(episode); 97 | } 98 | 99 | postMessage({json: json, podcastData: podcastData}); 100 | } 101 | 102 | onmessage = function (ev) { 103 | try { 104 | xmlParser(ev.data.xml, ev.data.feedUrl, ev.data.artwork); 105 | } catch(err) { 106 | console.log(err) 107 | } 108 | }; 109 | -------------------------------------------------------------------------------- /app/main.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow } = require('electron'); 2 | const path = require('path'); 3 | const url = require('url'); 4 | 5 | // Import getPreference() helper function 6 | const getPreference = require('./js/helper/helper_global'); 7 | /* 8 | // Modules to create app tray icon 9 | const Menu = require('electron').Menu 10 | const Tray = require('electron').Tray 11 | */ 12 | // Create variables for appIcon, trayIcon, win 13 | // to prevent their removal by garbage collection 14 | /* let appIcon = null */ 15 | let trayIcon = null; 16 | let win = null; 17 | 18 | // Request lock to allow only one instance 19 | // of the app running at the time. 20 | const gotTheLock = app.requestSingleInstanceLock(); 21 | /* 22 | // Load proper icon for specific platform 23 | if (process.platform === 'darwin') { 24 | trayIcon = path.join(__dirname, './img/tilde16x16.png') 25 | } else if (process.platform === 'linux') { 26 | trayIcon = path.join(__dirname, './img/tilde.png') 27 | } else if (process.platform === 'win32') { 28 | trayIcon = path.join(__dirname, './img/tilde.ico') 29 | } 30 | */ 31 | // Load proper icon for specific platform 32 | switch (process.platform) { 33 | case 'darwin': 34 | trayIcon = path.join(__dirname, './img/tilde16x16.png'); 35 | break; 36 | case 'linux': 37 | trayIcon = path.join(__dirname, './img/tilde.png'); 38 | break; 39 | case 'win32': 40 | trayIcon = path.join(__dirname, './img/tilde.ico'); 41 | break; 42 | } 43 | 44 | function createWindow() { 45 | win = new BrowserWindow({ 46 | width: 1100, 47 | minWidth: 1000, 48 | height: 640, 49 | minHeight: 620, //(process.platform === 'win32' ? 635 : 620), 50 | autoHideMenuBar: true, 51 | icon: trayIcon, 52 | frame: !(process.platform === "win32"), 53 | webPreferences: { 54 | nodeIntegration: true, 55 | enableRemoteModule: true, 56 | zoomFactor: 0.9 57 | }, 58 | show: false, 59 | backgroundColor: getPreference('darkmode') ? '#333' : '#fff' 60 | }); 61 | 62 | win.loadURL(url.format({ 63 | pathname: path.join(__dirname, 'index.html'), 64 | protocol: 'file:', 65 | slashed: true 66 | })); 67 | /* 68 | win.setBackgroundColor(getPreference('darkmode') ? '#333' : '#fff'); 69 | win.webContents.on('did-finish-load', function() { 70 | win.show(); 71 | }); */ 72 | /* 73 | // Create tray icon 74 | appIcon = new Tray(trayIcon) 75 | 76 | // Create RightClick context menu for tray icon 77 | const contextMenu = Menu.buildFromTemplate([ 78 | { 79 | label: 'Restore app', 80 | click: () => { 81 | win.show() 82 | } 83 | }, 84 | { 85 | label: 'Close app', 86 | click: () => { 87 | win.close() 88 | } 89 | } 90 | ]) 91 | 92 | // Set title for tray icon 93 | appIcon.setTitle('Tilde') 94 | 95 | // Set toot tip for tray icon 96 | appIcon.setToolTip('Tilde') 97 | 98 | // Create RightClick context menu 99 | appIcon.setContextMenu(contextMenu) 100 | 101 | // The tray icon is not destroyed 102 | appIcon.isDestroyed(false) 103 | 104 | // Restore (open) app after clicking on tray icon 105 | // if window is already open, minimize it to system tray 106 | appIcon.on('click', () => { 107 | win.isVisible() ? win.hide() : win.show() 108 | }) 109 | */ 110 | win.on('closed', function() { 111 | // Dereference the window object, usually you would store windows 112 | // in an array if your app supports multi windows, this is the time 113 | // when you should delete the corresponding element. 114 | win = null; 115 | }); 116 | /* 117 | // Minimize window to system tray if 'Minimize' option is checked 118 | if (getPreference('minimize') === true) { 119 | win.on('minimize', function(event){ 120 | event.preventDefault() 121 | // win.minimize() 122 | win.hide() 123 | }) 124 | } 125 | */ 126 | // Quit when all windows are closed 127 | win.on('window-all-closed', () => { 128 | app.quit(); 129 | }); 130 | 131 | win.on('closed', () => { 132 | app.quit(); 133 | }); 134 | 135 | /* // FUNCTIONS TO COMMUNICATE WITH THE RENDER 136 | function sendToRender(channel, obj) { 137 | const { ipcMain } = require('electron'); 138 | win.send(channel, obj); 139 | } 140 | 141 | function listenFromRender(channel, f) { 142 | const { ipcMain } = require('electron'); 143 | ipcMain.on(channel, async (event, obj) => { 144 | f(obj); 145 | }); 146 | } 147 | */ 148 | } 149 | 150 | // Check if this is first instance of the app running. 151 | // If not, block it. If yes, allow it. 152 | if(!gotTheLock) { 153 | app.quit(); 154 | } else { 155 | app.on('second-instance', (event, commandLine, workingDirectory) => { 156 | // Someone tried to run a second instance, we should focus our window. 157 | if(win) { 158 | if(win.isMinimized()) 159 | win.restore(); 160 | win.focus(); 161 | } 162 | }) 163 | 164 | // Create win, load the rest of the app, etc... 165 | app.on('ready', createWindow); 166 | } 167 | -------------------------------------------------------------------------------- /app/menu.js: -------------------------------------------------------------------------------- 1 | const { app, Menu } = require('electron').remote 2 | /* const { webFrame } = require('electron') */ 3 | 4 | const template = 5 | [/* 6 | { 7 | label: i18n.__('View'), 8 | submenu: 9 | [ 10 | {role: 'reload', label: i18n.__('Reload')}, 11 | {type: 'separator'}, 12 | {role: 'resetzoom', label: i18n.__('Reset Zoom')}, 13 | {role: 'zoomin', label: i18n.__('Zoom In'), accelerator: "CommandOrControl+="}, 14 | {role: 'zoomout', label: i18n.__('Zoom Out')}, 15 | {type: 'separator'}, 16 | { 17 | label: i18n.__('Dark Mode'), 18 | type: "checkbox", 19 | accelerator: "CommandOrControl+Shift+L", 20 | checked: getPreference('darkmode'), 21 | click() { 22 | changeThemeMode() 23 | updateUITheme() 24 | } 25 | }, 26 | {role: 'togglefullscreen', label: i18n.__('Toggle Full Screen')} 27 | ] 28 | }, */ 29 | { 30 | label: i18n.__('Player'), 31 | submenu: 32 | [ 33 | { 34 | label: i18n.__('Play/Pause'), 35 | accelerator: "CommandOrControl+Space", 36 | click() 37 | { 38 | if (document.activeElement.type == undefined) { 39 | playerManager.togglePlayPause("play-pause"); 40 | } 41 | } 42 | }, 43 | {type: 'separator'}, 44 | { 45 | label: i18n.__('30sec Reply'), 46 | accelerator: "CommandOrControl+Left", 47 | click() { playerManager.reply(); } 48 | }, 49 | { 50 | label: i18n.__("30sec Forward"), 51 | accelerator: "CommandOrControl+Right", 52 | click() { playerManager.forward(); } 53 | } 54 | ] 55 | }, 56 | { 57 | label: i18n.__('Go To'), 58 | submenu: 59 | [ 60 | { 61 | label: i18n.__("Search"), 62 | accelerator: "CommandOrControl+F", 63 | click() { showPage('search'); } 64 | }, 65 | {type: 'separator'}, 66 | { 67 | label: i18n.__("New Episodes"), 68 | accelerator: "CommandOrControl+1", 69 | click() { showPage('newEpisodes'); } 70 | }, 71 | { 72 | label: i18n.__("Favorites"), 73 | accelerator: "CommandOrControl+2", 74 | click() { showPage('favorites'); } 75 | }, 76 | { 77 | label: i18n.__("Settings"), 78 | accelerator: "CommandOrControl+3", 79 | click() { showPage('settings'); } 80 | }, 81 | { 82 | label: i18n.__("Archive"), 83 | accelerator: "CommandOrControl+4", 84 | click() { showPage('archive'); } 85 | }, 86 | { 87 | label: i18n.__("Statistics"), 88 | accelerator: "CommandOrControl+5", 89 | click() { showPage('statistics'); } 90 | }/* , 91 | {type: 'separator'}, 92 | { 93 | label: i18n.__("New List"), 94 | accelerator: "CommandOrControl+N", 95 | click() { focusTextField("new_list-input") } 96 | } */ 97 | ] 98 | }, 99 | { 100 | label: i18n.__('Settings'), 101 | submenu: 102 | [/* { 103 | label: i18n.__("Minimize"), 104 | type: "checkbox", 105 | checked: getPreference('minimize'), 106 | accelerator: "CommandOrControl+Shift+M", 107 | click() { 108 | changeMinimizeMenuItem() 109 | setMinimize() 110 | } 111 | }, */ 112 | { 113 | label: i18n.__('Dark Mode'), 114 | type: "checkbox", 115 | accelerator: "CommandOrControl+Shift+L", 116 | checked: getPreference('darkmode'), 117 | click() { 118 | changeThemeMode(); 119 | updateUITheme(); 120 | } 121 | }/* , 122 | {type: 'separator'}, 123 | {role: 'toggledevtools'} */ 124 | ] 125 | } 126 | ] 127 | /* 128 | if(process.platform === 'win32') { 129 | template[0].submenu.splice(2, 3, 130 | { 131 | label: 'Reset Zoom', 132 | accelerator: "CommandOrControl+O", 133 | click() { 134 | webFrame.setZoomFactor(1); 135 | } 136 | }, 137 | { 138 | label: 'Zoom In', 139 | accelerator: "CommandOrControl+=", 140 | click() { 141 | let zoomFactor = webFrame.getZoomFactor() + 0.1; 142 | if(zoomFactor < 2) 143 | webFrame.setZoomFactor(zoomFactor); 144 | } 145 | }, 146 | { 147 | label: 'Zoom Out', 148 | accelerator: "CommandOrControl+-", 149 | click() { 150 | let zoomFactor = webFrame.getZoomFactor() - 0.1; 151 | if(zoomFactor > 0) 152 | webFrame.setZoomFactor(zoomFactor); 153 | } 154 | } 155 | ); 156 | } 157 | */ 158 | 159 | if(isDevMode()) { 160 | template[template.length - 1].submenu.push({type: 'separator'}); 161 | template[template.length - 1].submenu.push({role: 'toggledevtools'}); 162 | } 163 | 164 | if (process.platform === 'darwin') { 165 | template.unshift({ 166 | label: app.getName(), 167 | submenu: [ 168 | {role: 'about'}, 169 | {type: 'separator'}, 170 | {role: 'services', submenu: []}, 171 | {type: 'separator'}, 172 | {role: 'hide'}, 173 | {role: 'hideothers'}, 174 | {role: 'unhide'}, 175 | {type: 'separator'}, 176 | {role: 'quit'} 177 | ] 178 | }) 179 | } 180 | 181 | const menu = Menu.buildFromTemplate(template) 182 | Menu.setApplicationMenu(menu) 183 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "main.js", 3 | "description": "Podcast client to listen to all you favorite podcasts", 4 | "name": "tilde-podcast", 5 | "version": "1.0.0", 6 | "license": "MIT", 7 | "author": "paologiuaa ", 8 | "homepage": "https://github.com/paologiua/tilde", 9 | "scripts": { 10 | "start": "npm install && electron main.js", 11 | "tilde": "electron main.js", 12 | "tilde-beta": "electron main.js dev", 13 | "pack": "electron-builder --dir", 14 | "dist": "electron-builder" 15 | }, 16 | "build": { 17 | "appId": "com.electron.tilde", 18 | "productName": "Tilde", 19 | "icon": "./img/tilde512x512.png", 20 | "mac": { 21 | "target": [ 22 | "dmg", 23 | "mas", 24 | "pkg", 25 | "zip" 26 | ], 27 | "category": "public.app-category.utilities", 28 | "icon": "./img/tilde.icns" 29 | }, 30 | "linux": { 31 | "target": [ 32 | "AppImage", 33 | "deb", 34 | "rpm", 35 | "snap", 36 | "zip" 37 | ], 38 | "icon": "./img/", 39 | "category": "Utility", 40 | "executableName": "tilde-podcast" 41 | }, 42 | "win": { 43 | "target": [ 44 | "nsis", 45 | "portable", 46 | "msi" 47 | ], 48 | "icon": "./img/tilde.ico" 49 | } 50 | }, 51 | "devDependencies": { 52 | "electron": "^10.1.5", 53 | "electron-builder": "^22.9.1", 54 | "node": "^12.16.3" 55 | }, 56 | "dependencies": { 57 | "custom-electron-titlebar": "^3.2.5", 58 | "jquery": "^3.5.1", 59 | "request": "^2.88.2", 60 | "request-progress": "^3.0.0" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/translations/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "Edit": "Bearbeiten", 3 | "Undo": "Rückgängig machen", 4 | "Redo": "Wiederholen", 5 | "Cut": "Ausschneiden", 6 | "Copy": "Kopieren", 7 | "Paste": "Einfügen", 8 | "Delete": "Löschen", 9 | "Select all": "Alles auswählen", 10 | "View": "Darstellung", 11 | "Go To": "Gehe zu", 12 | "Settings": "Einstellungen", 13 | "Player": "Wiedergabe", 14 | "Search": "Finde Podcast", 15 | "Favorites": "Favoriten", 16 | "History": "Verlauf", 17 | "Statistics": "Statistik", 18 | "Window": "Fenster", 19 | "Minimize": "Auf Systray minimieren (Neustart erforderlich)", 20 | "Close": "Schließen", 21 | "Quit": "Beenden", 22 | "New Episodes": "Neue Episoden", 23 | "Playlists": "Wiedergabelisten", 24 | "No episode selected": "Keine Episode ausgewählt", 25 | "New List": "Neue Liste", 26 | "Reload": "Aktualisieren", 27 | "Reset Zoom": "Reset Zoom", 28 | "Zoom In": "Zoom In", 29 | "Zoom Out": "Zoom Out", 30 | "Dark Mode": "Dunkler Modus", 31 | "Toggle Full Screen": "Full-Screen umschalten", 32 | "Play/Pause": "Play/Pause", 33 | "30sec Reply": "30sek zurück", 34 | "30sec Forward": "30sek forwärts", 35 | "Proxy Mode": "Proxy Modus", 36 | "Unsubscribe": "Deabonnieren", 37 | "Rename": "Umbenennen", 38 | "Add to playlist": "Zu Playlist hinzufügen", 39 | "Push to New Episodes": "Neuen Episoden automatisch hinzufügen", 40 | "History Items": "Einträge im Verlauf", 41 | "Favorite Podcasts": "Abonnierte Podcasts", 42 | "Last Podcast": "Letzter Podcast", 43 | "Episodes": "Episoden", 44 | "Refresh Feeds": "Feeds Aktualisieren", 45 | "Refresh":"Aktualisieren" 46 | } 47 | -------------------------------------------------------------------------------- /app/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Edit": "Edit", 3 | "Undo": "Undo", 4 | "Redo": "Redo", 5 | "Cut": "Cut", 6 | "Copy": "Copy", 7 | "Paste": "Paste", 8 | "Delete": "Delete", 9 | "Select all": "Select all", 10 | "View": "View", 11 | "Go To": "Go To", 12 | "Settings": "Settings", 13 | "Player": "Player", 14 | "Search": "Find podcast", 15 | "Favorites": "Favorites", 16 | "Archive": "Archive", 17 | "History": "History", 18 | "Statistics": "Statistics", 19 | "Window": "Window", 20 | "Minimize": "Minimize to systray (requires restart)", 21 | "Close": "Close", 22 | "Quit": "Quit", 23 | "New Episodes": "New Episodes", 24 | "Playlists": "Playlists", 25 | "No episode selected": "No episode selected", 26 | "New List": "New List", 27 | "Reload": "Reload", 28 | "Reset Zoom": "Reset Zoom", 29 | "Zoom In": "Zoom In", 30 | "Zoom Out": "Zoom Out", 31 | "Dark Mode": "Dark Mode", 32 | "Toggle Full Screen": "Toggle Full Screen", 33 | "Play/Pause": "Play/Pause", 34 | "30sec Reply": "30sec Reply", 35 | "30sec Forward": "30sec Forward", 36 | "Proxy Mode": "Proxy Mode", 37 | "Unsubscribe": "Unsubscribe", 38 | "Rename": "Rename", 39 | "Add to playlist": "Add to playlist", 40 | "Push to New Episodes": "Push to New Episodes", 41 | "Archive Items": "Archive Items", 42 | "History Items": "History Items", 43 | "Favorite Podcasts": "Favorite Podcasts", 44 | "Last Podcast": "Last Podcast", 45 | "Episodes": "Episodes", 46 | "Refresh Feeds": "Refresh Feeds", 47 | "Refresh":"Refresh", 48 | "Add to archive": "Add to archive", 49 | "Remove from archive": "Remove from archive", 50 | "Other": "Other", 51 | "Reduce": "Reduce", 52 | "Download in progress": "Download in progress", 53 | "Download completed": "Download completed", 54 | "Download error": "Download error", 55 | "Try typing the feed url into the search field!": "Try typing the feed url into the search field!" 56 | } 57 | -------------------------------------------------------------------------------- /app/translations/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "Edit": "Édition", 3 | "Undo": "Annuler", 4 | "Redo": "Refaire", 5 | "Cut": "Couper", 6 | "Copy": "Copier", 7 | "Paste": "Coller", 8 | "Delete": "Supprimer", 9 | "Select all": "Tout sélectioner", 10 | "View": "Affichage", 11 | "Go To": "Aller à", 12 | "Settings": "Options", 13 | "Player": "Lecteur", 14 | "Search": "Trouver un podcast", 15 | "Favorites": "Favoris", 16 | "History": "Historique", 17 | "Statistics": "Statistiques", 18 | "Window": "Fenêtre", 19 | "Minimize": "Mettre dans la barre de tâches (redémarrage obligatoire)", 20 | "Close": "Fermer", 21 | "Quit": "Quitter", 22 | "New Episodes": "Nouveaux épisodes", 23 | "Playlists": "Listes de lectures", 24 | "No episode selected": "Pas d'épisode sélectionné", 25 | "New List": "Nouvelle Liste", 26 | "Reload": "Rafraichir", 27 | "Reset Zoom": "RAZ Zoom", 28 | "Zoom In": "Zoomer", 29 | "Zoom Out": "Dézoomer", 30 | "Dark Mode": "Mode sombre", 31 | "Toggle Full Screen": "Mettre en plein écran", 32 | "Play/Pause": "Lecture/Pause", 33 | "30sec Reply": "30sec Précédente", 34 | "30sec Forward": "30sec Suivante", 35 | "Proxy Mode": "Mode Proxy", 36 | "Unsubscribe": "Se désinscrire", 37 | "Rename": "Renommer", 38 | "Add to playlist": "Ajouter à la liste de lecture", 39 | "Push to New Episodes": "Ajouter aux nouveaux épisodes", 40 | "History Items": "Éléments d'historique", 41 | "Favorite Podcasts": "Podcasts favoris", 42 | "Last Podcast": "Dernier Podcast", 43 | "Episodes": "Épisodes", 44 | "Refresh Feeds": "Rafraichir les flux", 45 | "Refresh":"Rafraichir" 46 | } -------------------------------------------------------------------------------- /app/translations/i18n.js: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | const electron = require('electron') 3 | const fs = require('fs'); 4 | 5 | let loadedLanguage; 6 | let app = electron.app ? electron.app : electron.remote.app 7 | 8 | module.exports = i18n; 9 | 10 | function i18n() 11 | { 12 | if(fs.existsSync(path.join(__dirname, app.getLocale() + '.json'))) 13 | { 14 | loadedLanguage = JSON.parse(fs.readFileSync(path.join(__dirname, app.getLocale() + '.json'), 'utf8')) 15 | } 16 | else 17 | { 18 | loadedLanguage = JSON.parse(fs.readFileSync(path.join(__dirname, 'en.json'), 'utf8')) 19 | } 20 | } 21 | 22 | i18n.prototype.__ = function(phrase) 23 | { 24 | let translation = loadedLanguage[phrase] 25 | 26 | if(translation === undefined) 27 | { 28 | translation = phrase 29 | } 30 | 31 | return translation 32 | } 33 | -------------------------------------------------------------------------------- /app/translations/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "Edit": "Modifica", 3 | "Undo": "Annulla", 4 | "Redo": "Rifare", 5 | "Cut": "Taglia", 6 | "Copy": "Copia", 7 | "Paste": "Incolla", 8 | "Delete": "Elimina", 9 | "Select all": "Seleziona tutto", 10 | "View": "Vista", 11 | "Go To": "Vai a", 12 | "Settings": "Impostazioni", 13 | "Player": "Player", 14 | "Search": "Cerca un Podcast", 15 | "Favorites": "Preferiti", 16 | "History": "Storia", 17 | "Archive": "Archivio", 18 | "Statistics": "Statistiche", 19 | "Window": "Finestra", 20 | "Minimize": "Riduci a System Tray (richiede riavvio)", 21 | "Close": "Chiudi", 22 | "Quit": "Esci", 23 | "New Episodes": "Nuovi episodi", 24 | "Playlists": "Playlists", 25 | "No episode selected": "Nessun episodio selezionato", 26 | "New List": "Nuova Playlist", 27 | "Reload": "Ricarica", 28 | "Reset Zoom": "Reset Zoom", 29 | "Zoom In": "Zoom In", 30 | "Zoom Out": "Zoom Out", 31 | "Dark Mode": "Dark Mode", 32 | "Toggle Full Screen": "Passa a schermo intero", 33 | "Play/Pause": "Play/Pause", 34 | "30sec Reply": "30sec indietro", 35 | "30sec Forward": "30sec avanti", 36 | "Proxy Mode": "Proxy Mode", 37 | "Unsubscribe": "Annulla iscrizione", 38 | "Rename": "Rinomina", 39 | "Add to playlist": "Aggiungi alla playlist", 40 | "Push to New Episodes": "Aggiungi a Nuovi episodi", 41 | "Archive Items": "Episodi archiviati", 42 | "History Items": "Episodi ascoltati", 43 | "Favorite Podcasts": "Podcasts preferiti", 44 | "Last Podcast": "Ultimo Podcast", 45 | "Episodes": "Episodi", 46 | "Refresh Feeds": "Ricarica Feeds", 47 | "Refresh":"Ricarica", 48 | "Done": "Fatto", 49 | "Set Playlist": "Imposta Playlist", 50 | "Nothing to show": "Niente da mostrare", 51 | "Show more episodes": "Mostra più episodi", 52 | "Show more recent episodes": "Mostra episodi più recenti", 53 | "Add to archive": "Archivia", 54 | "Remove from archive": "Rimuovi dall'archivio", 55 | "Other": "Altro", 56 | "Reduce": "Riduci", 57 | "Download in progress": "Download in corso", 58 | "Download completed": "Download completato", 59 | "Download error": "Errore durante il download", 60 | "Try typing the feed url into the search field!": "Prova a digitare il feed url nel campo di ricerca!" 61 | } 62 | -------------------------------------------------------------------------------- /app/translations/pt-BR.json: -------------------------------------------------------------------------------- 1 | { 2 | "Edit": "Editar", 3 | "Undo": "Desfazer", 4 | "Redo": "Refazer", 5 | "Cut": "Recortar", 6 | "Copy": "Copiar", 7 | "Paste": "Colar", 8 | "Delete": "Apagar", 9 | "Select all": "Selecionar Todos", 10 | "View": "Ver", 11 | "Go To": "Ir Para", 12 | "Settings": "Configurações", 13 | "Player": "Player", 14 | "Search": "Buscar", 15 | "Favorites": "Favoritos", 16 | "History": "Hístorico", 17 | "Statistics": "Estátisticas", 18 | "Window": "Encontre podcast", 19 | "Minimize": "Minimizar para systray (requer reinicialização)", 20 | "Close": "Fechar", 21 | "Quit": "Sair", 22 | "New Episodes": "Novos Epísodios", 23 | "Playlists":"Playlists", 24 | "No episode selected": "Nenhum epísodio selecionado", 25 | "New List": "Nova Playlist", 26 | "Reload": "Recarregar", 27 | "Reset Zoom":"Restaurar Zoom", 28 | "Zoom In": "Mais Zoom", 29 | "Zoom Out": "Menos Zoom", 30 | "Dark Mode": "Mode Escuro", 31 | "Toggle Full Screen": "Trocar tela cheia", 32 | "Play/Pause": "Play/Pause", 33 | "30sec Reply": "Voltar 30 seg", 34 | "30sec Forward":"Avançar 30 segundos", 35 | "Proxy Mode":"Modo Proxy", 36 | "Unsubscribe": "Unsubscribe", 37 | "Rename": "Rename", 38 | "Add to playlist": "Add to playlist", 39 | "Push to New Episodes": "Push to New Episodes", 40 | "History Items": "History Items", 41 | "Favorite Podcasts": "Favorite Podcasts", 42 | "Last Podcast": "Last Podcast", 43 | "Episodes": "Episodes", 44 | "Refresh": "Atualizar Feeds" 45 | } 46 | -------------------------------------------------------------------------------- /app/translations/pt-PT.json: -------------------------------------------------------------------------------- 1 | { 2 | "Edit": "Editar", 3 | "Undo": "Desfazer", 4 | "Redo": "Refazer", 5 | "Cut": "Recortar", 6 | "Copy": "Copiar", 7 | "Paste": "Colar", 8 | "Delete": "Apagar", 9 | "Select all": "Selecionar Todos", 10 | "View": "Ver", 11 | "Go To": "Ir Para", 12 | "Settings": "Configurações", 13 | "Player": "Player", 14 | "Search": "Buscar", 15 | "Favorites": "Favoritos", 16 | "History": "Hístorico", 17 | "Statistics": "Estátisticas", 18 | "Window": "Encontre podcast", 19 | "Minimize": "Minimizar para systray (requer reinicialização)", 20 | "Close": "Fechar", 21 | "Quit": "Sair", 22 | "New Episodes": "Novos Epísodios", 23 | "Playlists":"Playlists", 24 | "No episode selected": "Nenhum epísodio selecionado", 25 | "New List": "Nova Playlist", 26 | "Reload": "Recarregar", 27 | "Reset Zoom":"Restaurar Zoom", 28 | "Zoom In": "Mais Zoom", 29 | "Zoom Out": "Menos Zoom", 30 | "Dark Mode": "Mode Escuro", 31 | "Toggle Full Screen": "Trocar tela cheia", 32 | "Play/Pause": "Play/Pause", 33 | "30sec Reply": "Voltar 30 seg", 34 | "30sec Forward":"Avançar 30 segundos", 35 | "Proxy Mode":"Modo Proxy", 36 | "Unsubscribe": "Unsubscribe", 37 | "Rename": "Rename", 38 | "Add to playlist": "Add to playlist", 39 | "Push to New Episodes": "Push to New Episodes", 40 | "History Items": "History Items", 41 | "Favorite Podcasts": "Favorite Podcasts", 42 | "Last Podcast": "Last Podcast", 43 | "Episodes": "Episodes", 44 | "Refresh Feeds": "Atualizar Feeds" 45 | } 46 | -------------------------------------------------------------------------------- /images/logo_github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/images/logo_github.png -------------------------------------------------------------------------------- /images/screenshots/dark1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/images/screenshots/dark1.png -------------------------------------------------------------------------------- /images/screenshots/dark2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/images/screenshots/dark2.png -------------------------------------------------------------------------------- /images/screenshots/dark3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/images/screenshots/dark3.png -------------------------------------------------------------------------------- /images/screenshots/dark4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/images/screenshots/dark4.png -------------------------------------------------------------------------------- /images/screenshots/dark5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/images/screenshots/dark5.png -------------------------------------------------------------------------------- /images/screenshots/dark6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/images/screenshots/dark6.png -------------------------------------------------------------------------------- /images/screenshots/dark7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/images/screenshots/dark7.png -------------------------------------------------------------------------------- /images/screenshots/light1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/images/screenshots/light1.png -------------------------------------------------------------------------------- /images/screenshots/light2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/images/screenshots/light2.png -------------------------------------------------------------------------------- /images/screenshots/light3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/images/screenshots/light3.png -------------------------------------------------------------------------------- /images/screenshots/light4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/images/screenshots/light4.png -------------------------------------------------------------------------------- /images/screenshots/light5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/images/screenshots/light5.png -------------------------------------------------------------------------------- /images/screenshots/light6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/images/screenshots/light6.png -------------------------------------------------------------------------------- /images/screenshots/light7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/images/screenshots/light7.png -------------------------------------------------------------------------------- /images/screenshots/theme-old.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/images/screenshots/theme-old.gif -------------------------------------------------------------------------------- /images/screenshots/theme.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/images/screenshots/theme.gif -------------------------------------------------------------------------------- /images/tilde.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paologiua/tilde/44fb4f859a949af54f08126aa3106e1c89aeeb53/images/tilde.png --------------------------------------------------------------------------------