├── .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 | 
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 |
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 | 
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 | 
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 | 
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 | 
56 |
57 | # 📥 Archive
58 |
59 | You can keep the most interesting episodes in your personal archive.
60 |
61 | 
62 |
63 | # ⚙️ Settings
64 |
65 | In the settings you can choose the theme you prefer.
66 |
67 | 
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 |
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 |
--------------------------------------------------------------------------------
/app/img/ic_add_black_24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/img/ic_check_box_black_24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/img/ic_check_box_outline_blank_black_24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/img/ic_delete_black_24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/img/ic_favorite_black_24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/img/ic_favorite_border_black_24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/img/ic_forward_30_black_24px-old.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/img/ic_forward_30_black_24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/img/ic_history_black_24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/img/ic_insert_chart_black_24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/img/ic_more_horiz_black_24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/img/ic_pause_black_24px-old.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/img/ic_pause_black_24px.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/img/ic_play_arrow_black_24px-old.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/img/ic_play_arrow_black_24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/img/ic_playlist_add_black_24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/img/ic_playlist_play_black_24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/img/ic_refresh_black_24px.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/img/ic_remove_circle_black_24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/img/ic_replay_30_black_24px-old.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/img/ic_replay_30_black_24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/img/ic_search_black_24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/img/ic_skip_next_black_24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/img/ic_skip_previous_black_24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/img/ic_view_list_black_24px-old.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/img/ic_view_list_black_24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/img/ic_view_module_black_24px-old.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/img/ic_view_module_black_24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/img/icon.svg:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------
/app/img/nothing_found.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/app/img/playlist-icon.svg:
--------------------------------------------------------------------------------
1 |
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 |
20 |
--------------------------------------------------------------------------------
/app/img/volume-mute.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/img/volume-off.svg:
--------------------------------------------------------------------------------
1 |
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 |
52 |
53 | ${s_FullHeart}
54 |
55 |
56 | `).get(0);
57 | }
58 |
59 | function getStatisticsHeaderElement(title) {
60 | return $(`
61 |
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 '' +
150 | '' +
151 | i18n.__(text) +
152 | '' +
153 | '
';
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
--------------------------------------------------------------------------------