mirror of
				https://github.com/nkanaev/yarr.git
				synced 2025-10-31 06:53:30 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			366 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
			
		
		
	
	
			366 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
| <!DOCTYPE html>
 | |
| <html lang="en">
 | |
| <head>
 | |
|     <meta charset="UTF-8">
 | |
|     <title>yarr!</title>
 | |
|     <link rel="stylesheet" href="./static/stylesheets/bootstrap.min.css">
 | |
|     <link rel="stylesheet" href="./static/stylesheets/app.css">
 | |
|     <link rel="icon shortcut" href="./static/images/anchor.png">
 | |
| </head>
 | |
| <body class="theme-light">
 | |
|     <div class="wrapper d-flex vh-100" id="app" v-cloak>
 | |
|         <!-- feed list -->
 | |
|         <div class="vh-100 position-relative d-flex flex-column border-right flex-shrink-0" :style="{width: feedListWidth+'px'}">
 | |
|             <drag :width="feedListWidth" @resize="resizeFeedList"></drag>
 | |
|             <div class="p-2 toolbar d-flex align-items-center" style="padding-bottom: 1px;">
 | |
|                 <div class="icon mx-2" :class="{loading: loading.feeds}">{% inline "anchor.svg" %}</div>
 | |
|                 <div class="text-truncate cursor-default noselect">
 | |
|                     <span v-if="loading.feeds">Refreshing ({{loading.feeds}} left)</span>
 | |
|                 </div>
 | |
|                 <div class="flex-grow-1"></div>
 | |
|                 <div class="noselect" :class="{'d-none': loading.feeds}">
 | |
|                     <button class="toolbar-item"
 | |
|                             :class="{active: filterSelected == 'unread'}"
 | |
|                             v-b-tooltip.hover.bottom="'Unread'"
 | |
|                             @click="filterSelected = 'unread'">
 | |
|                         <span class="icon">{% inline "circle-full.svg" %}</span>
 | |
|                     </button>
 | |
|                     <button class="toolbar-item"
 | |
|                             :class="{active: filterSelected == 'starred'}"
 | |
|                             v-b-tooltip.hover.bottom="'Starred'"
 | |
|                             @click="filterSelected = 'starred'">
 | |
|                         <span class="icon">{% inline "star-full.svg" %}</span>
 | |
|                     </button>
 | |
|                     <button class="toolbar-item"
 | |
|                             :class="{active: filterSelected == ''}"
 | |
|                             v-b-tooltip.hover.bottom="'All'"
 | |
|                             @click="filterSelected = ''">
 | |
|                         <span class="icon">{% inline "assorted.svg" %}</span>
 | |
|                     </button>
 | |
|                 </div>
 | |
|                 <div class="flex-grow-1"></div>
 | |
|                 <b-dropdown
 | |
|                         right no-caret lazy="true" variant="link"
 | |
|                         class="settings-dropdown"
 | |
|                         toggle-class="toolbar-item px-2"
 | |
|                         ref="menuDropdown">
 | |
|                     <template v-slot:button-content class="toolbar-item">
 | |
|                         <span class="icon">{% inline "more-horizontal.svg" %}</span>
 | |
|                     </template>
 | |
|                     <b-dropdown-item-button @click="showSettings('create')">
 | |
|                         <span class="icon mr-1">{% inline "plus.svg" %}</span>
 | |
|                         New Feed
 | |
|                     </b-dropdown-item-button>
 | |
|                     <b-dropdown-item-button @click.stop="showSettings('manage')">
 | |
|                         <span class="icon mr-1">{% inline "list.svg" %}</span>
 | |
|                         Manage Feeds
 | |
|                     </b-dropdown-item-button>
 | |
|                     <b-dropdown-divider></b-dropdown-divider>
 | |
|                     <b-dropdown-item-button @click.stop="fetchAllFeeds()">
 | |
|                         <span class="icon mr-1">{% inline "rotate-cw.svg" %}</span>
 | |
|                         Refresh Feeds
 | |
|                     </b-dropdown-item-button>
 | |
|                     <b-dropdown-divider></b-dropdown-divider>
 | |
|                     <b-dropdown-header>Sort by</b-dropdown-header>
 | |
|                     <b-dropdown-item-button @click.stop="itemSortNewestFirst=true">
 | |
|                         <span class="icon mr-1" :class="{invisible: !itemSortNewestFirst}">{% inline "check.svg" %}</span>
 | |
|                         Newest First
 | |
|                     </b-dropdown-item-button>
 | |
|                     <b-dropdown-item-button @click="itemSortNewestFirst=false">
 | |
|                         <span class="icon mr-1" :class="{invisible: itemSortNewestFirst}">{% inline "check.svg" %}</span>
 | |
|                         Oldest First
 | |
|                     </b-dropdown-item-button>
 | |
|                     <b-dropdown-divider></b-dropdown-divider>
 | |
|                     <b-dropdown-header>Subscriptions</b-dropdown-header>
 | |
|                     <b-dropdown-form id="opml-import-form" enctype="multipart/form-data">
 | |
|                         <input type="file"
 | |
|                                id="opml-import"
 | |
|                                @change="importOPML"
 | |
|                                name="opml"
 | |
|                                style="opacity: 0; width: 1px; height: 0; position: absolute;">
 | |
|                         <label class="dropdown-item mb-0 cursor-pointer" for="opml-import">
 | |
|                             <span class="icon mr-1">{% inline "download.svg" %}</span>
 | |
|                             Import
 | |
|                         </label>
 | |
|                     </b-dropdown-form>
 | |
|                     <b-dropdown-item href="/opml/export">
 | |
|                         <span class="icon mr-1">{% inline "upload.svg" %}</span>
 | |
|                         Export
 | |
|                     </b-dropdown-item>
 | |
|                 </b-dropdown>
 | |
|             </div>
 | |
|             <div class="p-2 overflow-auto border-top">
 | |
|                 <label class="selectgroup">
 | |
|                     <input type="radio" name="feed" value="" v-model="feedSelected">
 | |
|                     <div class="selectgroup-label d-flex align-items-center w-100">
 | |
|                         <span class="icon mr-2">{% inline "layers.svg" %}</span>
 | |
|                         <span class="flex-fill text-left text-truncate" v-if="filterSelected=='unread'">All Unread</span>
 | |
|                         <span class="flex-fill text-left text-truncate" v-if="filterSelected=='starred'">All Starred</span>
 | |
|                         <span class="flex-fill text-left text-truncate" v-if="filterSelected==''">All Feeds</span>
 | |
|                         <span class="counter text-right">{{filteredTotalStats}}</span>
 | |
|                     </div>
 | |
|                 </label>
 | |
|                 <div v-for="folder in foldersWithFeeds">
 | |
|                     <label class="selectgroup mt-1"
 | |
|                            :class="{'d-none': filterSelected
 | |
|                                               && !filteredFolderStats[folder.id]
 | |
|                                               && (!itemSelected || feedsById[itemSelectedDetails.feed_id].folder_id != folder.id)}">
 | |
|                         <input type="radio" name="feed" :value="'folder:'+folder.id" v-model="feedSelected">
 | |
|                         <div class="selectgroup-label d-flex align-items-center w-100" v-if="folder.id">
 | |
|                             <span class="icon mr-2"
 | |
|                                   :class="{expanded: folder.is_expanded}"
 | |
|                                   @click.prevent="toggleFolderExpanded(folder)">
 | |
|                                 {% inline "chevron-right.svg" %}
 | |
|                             </span>
 | |
|                             <span class="flex-fill text-left text-truncate">{{ folder.title }}</span>
 | |
|                             <span class="counter text-right">{{filteredFolderStats[folder.id] || ''}}</span>
 | |
|                         </div>
 | |
|                     </label>
 | |
|                     <div v-show="!folder.id || folder.is_expanded" class="mt-1" :class="{'pl-3': folder.id}">
 | |
|                         <label class="selectgroup"
 | |
|                               :class="{'d-none': filterSelected
 | |
|                                                  && !filteredFeedStats[feed.id]
 | |
|                                                  && (!itemSelected || itemSelectedDetails.feed_id != feed.id)}"
 | |
|                                v-for="feed in folder.feeds">
 | |
|                             <input type="radio" name="feed" :value="'feed:'+feed.id" v-model="feedSelected">
 | |
|                             <div class="selectgroup-label d-flex align-items-center w-100">
 | |
|                                 <span class="icon mr-2" v-if="!feed.has_icon">{% inline "rss.svg" %}</span>
 | |
|                                 <span class="icon mr-2" v-else><img :src="'/api/feeds/'+feed.id+'/icon'" alt=""></span>
 | |
|                                 <span class="flex-fill text-left text-truncate">{{ feed.title }}</span>
 | |
|                                 <span class="counter text-right">{{filteredFeedStats[feed.id] || ''}}</span>
 | |
|                             </div>
 | |
|                         </label>
 | |
|                     </div>
 | |
|                 </div>
 | |
|             </div>
 | |
|         </div>
 | |
|         <!-- item list -->
 | |
|         <div class="vh-100 position-relative d-flex flex-column border-right flex-shrink-0"  :style="{width: itemListWidth+'px'}">
 | |
|             <drag :width="itemListWidth" @resize="resizeItemList"></drag>
 | |
|             <div class="px-2 toolbar d-flex align-items-center">
 | |
|                 <div class="input-icon flex-grow-1">
 | |
|                     <span class="icon">{% inline "search.svg" %}</span>
 | |
|                     <input class="d-block toolbar-search" type="" v-model="itemSearch">
 | |
|                 </div>
 | |
|                 <button class="toolbar-item ml-2"
 | |
|                         @click="markItemsRead()"
 | |
|                         v-if="filterSelected == 'unread'"
 | |
|                         v-b-tooltip.hover.bottom="'Mark All Read'">
 | |
|                     <span class="icon">{% inline "check.svg" %}</span>
 | |
|                 </button>
 | |
|             </div>
 | |
|             <div class="p-2 overflow-auto border-top" v-scroll="loadMoreItems">
 | |
|                 <label v-for="item in items" :key="item.id"
 | |
|                        class="selectgroup">
 | |
|                     <input type="radio" name="item" :value="item.id" v-model="itemSelected">
 | |
|                     <div class="selectgroup-label d-flex flex-column">
 | |
|                         <div style="line-height: 1; opacity: .7; margin-bottom: .1rem;" class="d-flex align-items-center">
 | |
|                             <transition name="indicator">
 | |
|                                 <span class="icon icon-small mr-1" v-if="item.status=='unread'">{% inline "circle-full.svg" %}</span>
 | |
|                                 <span class="icon icon-small mr-1" v-if="item.status=='starred'">{% inline "star-full.svg" %}</span>
 | |
|                             </transition>
 | |
|                             <small class="flex-fill text-truncate mr-1">
 | |
|                                 {{feedsById[item.feed_id].title}}
 | |
|                             </small>
 | |
|                             <small class="flex-shrink-0"><relative-time :val="item.date"/></small>
 | |
|                         </div>
 | |
|                         <div>{{item.title}}</div>
 | |
|                     </div>
 | |
|                 </label>
 | |
|                 <button class="btn btn-link btn-block loading my-3" v-if="itemsPage.cur < itemsPage.num"></button>
 | |
|             </div>
 | |
|         </div>
 | |
|         <!-- item show -->
 | |
|         <div class="vh-100 d-flex flex-column w-100" style="min-width: 0;">
 | |
|             <div class="toolbar px-2 d-flex align-items-center" v-if="itemSelected">
 | |
|                 <button class="toolbar-item"
 | |
|                         @click="toggleItemStarred(itemSelectedDetails)"
 | |
|                         v-b-tooltip.hover.bottom="'Mark Starred'">
 | |
|                     <span class="icon" v-if="itemSelectedDetails.status=='starred'" >{% inline "star-full.svg" %}</span>
 | |
|                     <span class="icon" v-else-if="itemSelectedDetails.status!='starred'" >{% inline "star.svg" %}</span>
 | |
|                 </button>
 | |
|                 <button class="toolbar-item"
 | |
|                         :disabled="itemSelectedDetails.status=='starred'"
 | |
|                         v-b-tooltip.hover.bottom="'Mark Unread'"
 | |
|                         @click="toggleItemRead(itemSelectedDetails)">
 | |
|                     <span class="icon" v-if="itemSelectedDetails.status=='unread'">{% inline "circle-full.svg" %}</span>
 | |
|                     <span class="icon" v-if="itemSelectedDetails.status!='unread'">{% inline "circle.svg" %}</span>
 | |
|                 </button>
 | |
|                 <button class="toolbar-item" id="content-appearance" v-b-tooltip.hover.bottom="'Appearance'">
 | |
|                     <span class="icon">{% inline "sliders.svg" %}</span>
 | |
|                 </button>
 | |
|                 <button class="toolbar-item" @click="getReadable(itemSelectedDetails)" v-b-tooltip.hover.bottom="'Read Here'">
 | |
|                     <span class="icon">{% inline "book-open.svg" %}</span>
 | |
|                 </button>
 | |
|                 <a class="toolbar-item" :href="itemSelectedDetails.link" target="_blank" v-b-tooltip.hover.bottom="'Open Link'">
 | |
|                     <span class="icon">{% inline "external-link.svg" %}</span>
 | |
|                 </a>
 | |
|                 <b-popover target="content-appearance" triggers="focus" placement="bottom">
 | |
|                     <div class="p-1" style="width: 200px;">
 | |
|                         <div class="d-flex">
 | |
|                             <label class="themepicker">
 | |
|                                 <input type="radio" name="settingsTheme" value="light" v-model="theme.name">
 | |
|                                 <div class="themepicker-label appearance-option"></div>
 | |
|                             </label>
 | |
|                             <label class="themepicker">
 | |
|                                 <input type="radio" name="settingsTheme" value="sepia" v-model="theme.name">
 | |
|                                 <div class="themepicker-label appearance-option"></div>
 | |
|                             </label>
 | |
|                             <label class="themepicker">
 | |
|                                 <input type="radio" name="settingsTheme" value="night" v-model="theme.name">
 | |
|                                 <div class="themepicker-label appearance-option"></div>
 | |
|                             </label>
 | |
|                         </div>
 | |
|                         <div class="mt-2">
 | |
|                             <label class="selectgroup">
 | |
|                                 <input type="radio" name="font" value="" v-model="theme.font">
 | |
|                                 <div class="selectgroup-label appearance-option">
 | |
|                                     System Default
 | |
|                                 </div>
 | |
|                             </label>
 | |
|                             <label class="selectgroup" v-for="f in fonts" :key="f">
 | |
|                                 <input type="radio" name="font" :value="f" v-model="theme.font">
 | |
|                                 <div class="selectgroup-label appearance-option":style="{'font-family': f}">
 | |
|                                     {{ f }}
 | |
|                                 </div>
 | |
|                             </label>
 | |
|                         </div>
 | |
|                         <div class="btn-group d-flex mt-2">
 | |
|                             <button class="btn btn-outline appearance-option"
 | |
|                                     style="font-size: 0.8rem" @click="incrFont(-1)">A</button>
 | |
|                             <button class="btn btn-outline appearance-option"
 | |
|                                     style="font-size: 1.2rem" @click="incrFont(1)">A</button>
 | |
|                         </div>
 | |
|                     </div>
 | |
|                 </b-popover>
 | |
|                 <div class="flex-grow-1"></div>
 | |
|                 <button class="toolbar-item" @click="itemSelected=null" v-b-tooltip.hover.bottom="'Close Article'">
 | |
|                     <span class="icon">{% inline "x.svg" %}</span>
 | |
|                 </button>
 | |
|             </div>
 | |
|             <div v-if="itemSelected"
 | |
|                  ref="content"
 | |
|                  class="content px-4 pt-3 pb-5 border-top overflow-auto"
 | |
|                  :style="{'font-family': theme.font, 'font-size': theme.size + 'rem'}">
 | |
|                 <h1><b>{{itemSelectedDetails.title}}</b></h1>
 | |
|                 <div class="text-muted">
 | |
|                     <div>{{ feedsById[itemSelectedDetails.feed_id].title }}</div>
 | |
|                     <time>{{ formatDate(itemSelectedDetails.date) }}</time>
 | |
|                 </div>
 | |
|                 <hr>
 | |
|                 <div v-html="itemSelectedContent"></div>
 | |
|             </div>
 | |
|         </div>
 | |
|         <b-modal id="settings-modal" hide-header hide-footer lazy>
 | |
|             <button class="btn btn-link outline-none float-right p-2 mr-n2 mt-n2" style="line-height: 1" @click="$bvModal.hide('settings-modal')">
 | |
|                 <span class="icon">{% inline "x.svg" %}</span>
 | |
|             </button>
 | |
|             <div v-if="settings=='create'">
 | |
|                 <p class="cursor-default"><b>New Feed</b></p>
 | |
|                 <form action="" @submit.prevent="createFeed(event)" class="mt-4">
 | |
|                     <label for="feed-url">URL</label>
 | |
|                     <input id="feed-url" name="url" type="url" class="form-control" required autocomplete="off" :readonly="feedNewChoice.length > 0">
 | |
|                     <label for="feed-folder" class="mt-3 d-block">
 | |
|                         Folder
 | |
|                         <a href="#" class="float-right text-decoration-none" @click.prevent="createNewFeedFolder()">new folder</a>
 | |
|                     </label>
 | |
|                     <select class="form-control" id="feed-folder" name="folder_id" ref="newFeedFolder">
 | |
|                         <option value="">---</option>
 | |
|                         <option :value="folder.id" v-for="folder in folders">{{ folder.title }}</option>
 | |
|                     </select>
 | |
|                     <div class="mt-4" v-if="feedNewChoice.length">
 | |
|                         <p class="mb-2">
 | |
|                             Multiple feeds found. Choose one below:
 | |
|                             <a href="#" class="float-right text-decoration-none" @click.prevent="resetFeedChoice()">cancel</a>
 | |
|                         </p>
 | |
|                         <label class="selectgroup" v-for="choice in feedNewChoice">
 | |
|                             <input type="radio" name="feedToAdd" :value="choice.url" v-model="feedNewChoiceSelected">
 | |
|                             <div class="selectgroup-label">
 | |
|                                 <div>{{ choice.title }}</div>
 | |
|                                 <div :class="{light: choice.title}">{{ choice.url }}</div>
 | |
|                             </div>
 | |
|                         </label>
 | |
|                     </div>
 | |
|                     <button class="btn btn-block btn-default mt-3" :class="{loading: loading.newfeed}" type="submit">Add</button>
 | |
|                 </form>
 | |
|             </div>
 | |
|             <div v-else-if="settings=='manage'">
 | |
|                 <p class="cursor-default"><b>Manage Feeds</b></p>
 | |
|                 <div v-for="folder in foldersWithFeeds" class="mt-4" :key="folder.id">
 | |
|                     <div class="list-row d-flex align-items-center">
 | |
|                         <div class="w-100 text-truncate" v-if="folder.id">
 | |
|                             <span class="icon mr-2">{% inline "folder.svg" %}</span>
 | |
|                             {{ folder.title }}
 | |
|                         </div>
 | |
|                         <div class="flex-shrink-0" v-if="folder.id">
 | |
|                             <b-dropdown right no-caret lazy="true" variant="link" class="settings-dropdown" toggle-class="text-decoration-none">
 | |
|                                 <template v-slot:button-content>
 | |
|                                     <span class="icon">{% inline "more-vertical.svg" %}</span>
 | |
|                                 </template>
 | |
|                                 <b-dropdown-header>{{folder.title}}</b-dropdown-header>
 | |
|                                 <b-dropdown-item @click.prevent="renameFolder(folder)">Rename</b-dropdown-item>
 | |
|                                 <b-dropdown-divider></b-dropdown-divider>
 | |
|                                 <b-dropdown-item class="dropdown-danger"
 | |
|                                    @click.prevent="deleteFolder(folder)">
 | |
|                                     Delete
 | |
|                                 </b-dropdown-item>
 | |
|                             </b-dropdown>
 | |
|                         </div>
 | |
|                     </div>
 | |
|                     <div v-for="feed in folder.feeds" class="list-row d-flex align-items-center" :key="feed.id">
 | |
|                         <div class="w-100 text-truncate">
 | |
|                             <span class="icon mr-2" v-if="!feed.has_icon">{% inline "rss.svg" %}</span>
 | |
|                             <span class="icon mr-2" v-else><img :src="'/api/feeds/'+feed.id+'/icon'" alt=""></span>
 | |
|                             {{ feed.title }}
 | |
|                         </div>
 | |
|                         <div class="flex-shrink-0">
 | |
|                             <b-dropdown right no-caret lazy="true" variant="link" class="settings-dropdown" toggle-class="text-decoration-none">
 | |
|                                 <template v-slot:button-content>
 | |
|                                     <span class="icon">{% inline "more-vertical.svg" %}</span>
 | |
|                                 </template>
 | |
|                                 <b-dropdown-header>{{feed.title}}</b-dropdown-header>
 | |
|                                 <b-dropdown-item :href="feed.link" target="_blank" v-if="feed.link">Visit Website</b-dropdown-item>
 | |
|                                 <b-dropdown-divider v-if="feed.link"></b-dropdown-divider>
 | |
|                                 <b-dropdown-item @click.prevent="renameFeed(feed)">Rename</b-dropdown-item>
 | |
|                                 <b-dropdown-divider v-if="folders.length"></b-dropdown-divider>
 | |
|                                 <b-dropdown-header v-if="folders.length">Move to...</b-dropdown-header>
 | |
|                                 <b-dropdown-item @click="moveFeed(feed, null)" v-if="feed.folder_id">
 | |
|                                     ---
 | |
|                                 </b-dropdown-item>
 | |
|                                 <b-dropdown-item-button
 | |
|                                     v-if="folder.id != feed.folder_id"
 | |
|                                     v-for="folder in folders"
 | |
|                                     @click="moveFeed(feed, folder)">
 | |
|                                     <span class="icon mr-1">{% inline "folder.svg" %}</span>
 | |
|                                     {{ folder.title }}
 | |
|                                 </b-dropdown-item-button>
 | |
|                                 <b-dropdown-item-button @click="moveFeedToNewFolder(feed)">
 | |
|                                     <span class="text-muted icon mr-1">{% inline "plus.svg" %}</span>
 | |
|                                     <span class="text-muted">New Folder</span>
 | |
|                                 </b-dropdown-item-button>
 | |
|                                 <b-dropdown-divider></b-dropdown-divider>
 | |
|                                 <b-dropdown-item class="dropdown-danger"
 | |
|                                    @click.prevent="deleteFeed(feed)">
 | |
|                                     Delete
 | |
|                                 </b-dropdown-item>
 | |
|                             </b-dropdown>
 | |
|                         </div>
 | |
|                     </div>
 | |
|                 </div>
 | |
|             </div>
 | |
|         </div>
 | |
|     </div>
 | |
|     <script src="./static/javascripts/vue.min.js"></script>
 | |
|     <script src="./static/javascripts/popper.min.js"></script>
 | |
|     <!-- <script src="./static/javascripts/bootstrap.min.js"></script> -->
 | |
|     <script src="./static/javascripts/bootstrap-vue.min.js"></script>
 | |
|     <script src="./static/javascripts/Readability.js"></script>
 | |
|     <script src="./static/javascripts/purify.min.js"></script>
 | |
|     <script src="./static/javascripts/api.js"></script>
 | |
|     <script src="./static/javascripts/app.js"></script>
 | |
|     <!--[if IE ]>
 | |
|     <script src="./static/javascripts/url-polyfill.min.js"></script>
 | |
|     <![endif]-->
 | |
| </body>
 | |
| </html>
 |