Fixed an layout invalidation bug where we were discarding real invalidations.
Allow to defer `<img defer src>` (useful for local tests).
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Self-hosted Roboto (variable font, weight axis 100–900).
|
||||||
|
* Source: Google Fonts v51. Two subsets retained: latin and latin-ext.
|
||||||
|
* Loaded locally to keep DNS / TLS dependencies off fonts.googleapis.com
|
||||||
|
* and fonts.gstatic.com.
|
||||||
|
*
|
||||||
|
* font-display: swap — render fallback text immediately, swap when ready.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100 900;
|
||||||
|
font-stretch: 100%;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/assets/fonts/roboto-latin-ext.woff2') format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100 900;
|
||||||
|
font-stretch: 100%;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/assets/fonts/roboto-latin.woff2') format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 786 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 737 B |
|
After Width: | Height: | Size: 765 B |
@@ -0,0 +1,397 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
|
<title>
|
||||||
|
Story Archive - NewsBlur
|
||||||
|
</title>
|
||||||
|
<link rel="shortcut icon" href="https://newsblur.com/media/img/favicon.ico" type="image/png">
|
||||||
|
<link rel="icon" href="https://newsblur.com/media/img/favicon_32.png" sizes="32x32">
|
||||||
|
<link rel="icon" href="https://newsblur.com/media/img/favicon_64.png" sizes="64x64">
|
||||||
|
<link rel="apple-touch-icon" href="https://newsblur.com/media/img/mobile/apple-touch-icon-precomposed.png">
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="msapplication-TileImage" content="/media/img/logo_144.png">
|
||||||
|
<meta name="msapplication-TileColor" content="#F7E5A8">
|
||||||
|
<meta name="apple-itunes-app" content="app-id=463981119">
|
||||||
|
<link rel="manifest" href="https://newsblur.com/manifest.webmanifest">
|
||||||
|
<link rel="canonical" href="https://www.newsblur.com/features/archive">
|
||||||
|
|
||||||
|
<meta name="description" content="Every story archived and searchable forever. Your personal knowledge base. When you subscribe to a feed, NewsBlur back-fills its entire history. Stories never expire.">
|
||||||
|
<meta property="og:title" content="Story Archive - NewsBlur">
|
||||||
|
<meta property="og:description" content="Every story archived and searchable forever. Your personal knowledge base.">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:url" content="https://www.newsblur.com/features/archive">
|
||||||
|
<meta property="og:image" content="https://www.newsblur.com/media/img/features/archive.png">
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:site" content="@newsblur">
|
||||||
|
<meta name="twitter:title" content="Story Archive - NewsBlur">
|
||||||
|
<meta name="twitter:description" content="Every story archived and searchable forever. Your personal knowledge base.">
|
||||||
|
<meta name="twitter:image" content="https://www.newsblur.com/media/img/features/archive.png">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- <link rel="mask-icon"
|
||||||
|
href="/media/img/favicon_monochrome.svg"
|
||||||
|
color="orange"> -->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body{height:100%;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-webkit-overflow-scrolling:touch;scrollbar-color:#c2c2c2 #fafafa;background-color:#fff;border:0;outline:0;margin:0;padding:0;line-height:1.3;text-decoration:none;list-style:none;overflow:hidden}
|
||||||
|
a,a:active,a:hover,a:visited,button{outline:none}a img{border:none}
|
||||||
|
.NB-splash-bottom{display:block}
|
||||||
|
body,.NB-theme-feed-font-whitney{font-family:Whitney SSm A,Whitney SSm B,Lucida Grande,Verdana,Helvetica Neue,Helvetica,sans-serif}
|
||||||
|
.NB-splash-info{z-index:1;width:100%;height:55px;position:absolute;bottom:0;right:0}.NB-splash-info.NB-splash-top{top:0;bottom:inherit;border-bottom:1px solid #0000001a}
|
||||||
|
.NB-splash-info.NB-splash-bottom{-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);background:#f3f5f1b3;border-top:1px solid #0000001a;height:36px;overflow:hidden}
|
||||||
|
.NB-splash-info .NB-splash-links{float:right;text-shadow:1px 1px #f0f0f0;white-space:nowrap;z-index:1;margin:0;padding:0;font-weight:300;line-height:16px;list-style:none;display:block}.NB-splash-info .NB-splash-links .NB-splash-link{float:left;height:36px;margin:0;line-height:12px;display:block;overflow:hidden}.NB-splash-info .NB-splash-links .NB-splash-link a{text-rendering:optimizeLegibility;background:url(/media/embed/reader/spacer_16.png) 0 -17px/16px no-repeat;margin:0;padding:12px 12px 12px 20px;font-weight:700;transition:background .36s ease-out;display:block}
|
||||||
|
.NB-splash-info .NB-splash-link-logo{display:none}
|
||||||
|
.NB-splash-blurred-logo{background:url(/media/embed/logo_newsblur_blur.png) 50%/contain no-repeat;width:146px;height:24px;margin-top:6px}
|
||||||
|
.NB-splash-info .NB-splash-links .NB-splash-link.NB-active a,.NB-splash-info .NB-splash-links .NB-splash-link-about a:hover,.NB-splash-info .NB-splash-links .NB-splash-link-faq a:hover,.NB-splash-info .NB-splash-links .NB-splash-link-api a:hover,.NB-splash-info .NB-splash-links .NB-splash-link-press a:hover{background:url(/media/embed/favicon_32.png) 0 10px/16px no-repeat}
|
||||||
|
.NB-splash-info .NB-splash-links a{font-size:11px;font-weight:700;display:block}.NB-splash-info .NB-splash-links a,.NB-static a,.NB-splash-link{color:#405ba8;font-weight:700;text-decoration:none}
|
||||||
|
.NB-static{color:#3a3122;background:linear-gradient(#f5eedd 0%,#f0e7cf 20%,#ebe0c2 40%,#e5d9b6 60%,#e0d2ab 80%,#dbcca2 100%);min-height:100vh;padding:94px 0 0;overflow:auto}.NB-static h3{color:#2d261a;line-height:1.4em}.NB-static a{color:#8a6d1b;font-weight:700;text-decoration:none}
|
||||||
|
.NB-static .NB-splash-info{-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);background-color:#243636d9;border-bottom:1px solid #ffffff14;position:fixed}.NB-static .NB-splash-info.NB-splash-bottom{-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);background:#243636e6;border-top:1px solid #ffffff14;border-bottom:none}.NB-static .NB-splash-info .NB-splash-links{text-shadow:none}.NB-static .NB-splash-info .NB-splash-links .NB-splash-link a{color:#ffffff8c}
|
||||||
|
.NB-static .NB-splash-info .NB-splash-links .NB-splash-link.NB-active a{color:#fbdb9b;background:url(/media/embed/favicon_32.png) 0 10px/16px no-repeat}
|
||||||
|
.NB-static ul{margin-left:0;padding-left:20px}.NB-static li{color:#2d261ab3;margin-bottom:6px;list-style:square}
|
||||||
|
.NB-static h3{color:#2d261a;margin-top:36px;font-size:24px}
|
||||||
|
.NB-static-pricing .NB-body-inner,.NB-static-features .NB-body-inner,.NB-static-compare .NB-body-inner,.NB-static-about .NB-body-inner,.NB-static-faq .NB-body-inner,.NB-static-api .NB-body-inner,.NB-static-forgot-password .NB-body-inner{max-width:1200px;margin:0 auto;padding:96px 32px 48px}.NB-static .NB-splash-info.NB-splash-top{height:70px;left:0}.NB-splash-top-nav{display:none}.NB-static .NB-splash-top-nav{box-sizing:border-box;align-items:center;width:100%;max-width:1200px;height:100%;margin:0 auto;padding:0 32px;display:flex}.NB-static .NB-splash-top-title{color:#2d261a;white-space:nowrap;text-align:left;flex:1;min-width:0;font-family:Gotham Narrow A,Gotham Narrow B,sans-serif;font-size:24px;font-weight:700}body.NB-dark.NB-static .NB-splash-top-title{color:#fff}
|
||||||
|
.NB-splash-top-links{display:none}.NB-static .NB-splash-top-links{-webkit-padding-start:0;flex-shrink:0;justify-content:center;align-items:center;gap:4px;margin:0;padding:0;list-style:none;display:flex}.NB-splash-top-logo{display:none}.NB-static .NB-splash-top-logo{flex:1;justify-content:flex-end;align-items:center;gap:16px;display:flex}.NB-static .NB-splash-top-logo .NB-splash-blurred-logo{width:146px;height:28px;margin:0}.NB-static .NB-splash-top-logo-fallback,.NB-welcome .NB-splash-top-logo-fallback,.NB-mobile-logo-link{display:none}.NB-splash-top-link{padding:0;list-style:none;display:block}.NB-static .NB-splash-top-link,.NB-welcome .NB-splash-top-link{margin:0}
|
||||||
|
.NB-static .NB-splash-top-link a{letter-spacing:.3px;color:#2d261a99;background:0 0;border:1px solid #0000;border-radius:6px;padding:6px 14px;font-family:Gotham Narrow A,Gotham Narrow B,sans-serif;font-size:14px;font-weight:600;line-height:1;text-decoration:none;transition:color .15s,background .15s,border-color .15s;display:block}
|
||||||
|
.NB-static-features .NB-splash-top-link-features>a,.NB-static-pricing .NB-splash-top-link-pricing>a,.NB-static-about .NB-splash-top-link-about>a,.NB-static-faq .NB-splash-top-link-faq>a,.NB-static-compare .NB-splash-top-link-features>a{color:#2d261a;border-color:#2d261a33}
|
||||||
|
.NB-static .NB-splash-blurred-logo{background-image:url(/media/embed/logo_newsblur_blur.png)}body.NB-dark.NB-static .NB-splash-blurred-logo{background-image:url(/media/embed/logo_newsblur_blur_dark.png)}
|
||||||
|
.NB-splash-nav-has-dropdown{position:relative}.NB-splash-nav-has-dropdown>a:after{content:"";vertical-align:middle;opacity:.6;border-top:4px solid;border-left:3.5px solid #0000;border-right:3.5px solid #0000;width:0;height:0;margin-left:5px;transition:opacity .15s,transform .15s;display:inline-block;position:relative;top:-.5px}
|
||||||
|
.NB-splash-dropdown{opacity:0;visibility:hidden;pointer-events:none;padding-top:8px;transition:opacity .2s,visibility .2s;position:fixed;top:0;left:0}.NB-splash-dropdown-features{width:652px}.NB-splash-dropdown:not(.NB-splash-dropdown-features){width:312px}
|
||||||
|
.NB-splash-dropdown:before{content:"";height:20px;position:absolute;top:-12px;left:0;right:0}.NB-splash-dropdown-inner{-webkit-backdrop-filter:blur(40px)saturate(1.4);backdrop-filter:blur(40px)saturate(1.4);background:#fffffff2;border:1px solid #ffffffb3;border-radius:12px;min-width:280px;padding:12px;box-shadow:0 12px 40px #0000001a,0 0 0 1px #00000008}.NB-splash-dropdown-features .NB-splash-dropdown-inner{gap:0;min-width:620px;padding:16px;display:flex}.NB-splash-dropdown-col-hero{border-right:1px solid #00000014;flex:0 0 290px;padding-right:16px}.NB-splash-dropdown-col-more{flex:1;min-width:240px;padding-left:16px}.NB-splash-dropdown-heading{letter-spacing:1px;text-transform:uppercase;color:#1a150880;margin:0;padding:4px 10px 8px;font-family:Gotham Narrow A,Gotham Narrow B,sans-serif;font-size:10px;font-weight:700}a.NB-splash-dropdown-heading-link,a.NB-splash-dropdown-heading-link:link,a.NB-splash-dropdown-heading-link:visited,.NB-splash-dropdown-heading-link{color:#1a1508;letter-spacing:1px;text-transform:uppercase;font-family:Gotham Narrow A,Gotham Narrow B,sans-serif;font-size:10px;font-weight:700;text-decoration:none;transition:color .15s}
|
||||||
|
.NB-splash-dropdown-item{cursor:pointer;border-radius:8px;padding:8px 10px;text-decoration:none;transition:background .15s;display:block}.NB-splash-dropdown-icon{vertical-align:-2px;width:16px;height:16px;margin-right:6px}.NB-splash-dropdown-icon-blue{filter:invert(37%)sepia(93%)saturate(2000%)hue-rotate(200deg)brightness(1.1)}.NB-splash-dropdown-icon-coral{filter:invert(50%)sepia(80%)saturate(1200%)hue-rotate(340deg)brightness(1.1)}.NB-splash-dropdown-icon-purple{filter:invert(30%)sepia(90%)saturate(2500%)hue-rotate(260deg)brightness(1.1)}.NB-splash-dropdown-icon-pink{filter:invert(45%)sepia(90%)saturate(1500%)hue-rotate(310deg)brightness(1.1)}.NB-splash-dropdown-icon-teal{filter:invert(55%)sepia(75%)saturate(1500%)hue-rotate(140deg)brightness(1.1)}.NB-splash-dropdown-icon-orange{filter:invert(57%)sepia(98%)saturate(1000%)hue-rotate(360deg)brightness(1.1)}.NB-splash-dropdown-icon-green{filter:invert(55%)sepia(75%)saturate(1500%)hue-rotate(90deg)brightness(1.1)}.NB-splash-dropdown-icon-gold{filter:invert(60%)sepia(90%)saturate(1200%)hue-rotate(20deg)brightness(1.1)}.NB-splash-dropdown-icon-indigo{filter:invert(25%)sepia(90%)saturate(2500%)hue-rotate(235deg)brightness(.9)}.NB-splash-dropdown-icon-cyan{filter:invert(55%)sepia(85%)saturate(1800%)hue-rotate(160deg)brightness(1.1)}a.NB-splash-dropdown-item,a.NB-splash-dropdown-item:link,a.NB-splash-dropdown-item:visited,.NB-splash-dropdown-item{color:#2d261a;text-decoration:none}
|
||||||
|
.NB-splash-dropdown-item-name{color:#2d261a;margin-bottom:2px;font-family:Gotham Narrow A,Gotham Narrow B,sans-serif;font-size:13px;font-weight:600;line-height:1.2}
|
||||||
|
.NB-splash-dropdown-item-desc{color:#2d261aa6;font-family:Gotham Narrow A,Gotham Narrow B,sans-serif;font-size:11px;font-weight:400;line-height:1.3}.NB-splash-dropdown-price{color:#5a4510;margin-left:4px;font-size:11px;font-weight:600}.NB-splash-dropdown-tier{padding-left:18px;position:relative}.NB-splash-dropdown-tier:before{content:"";background:0 0;border-radius:3px;width:4px;height:60%;transition:background .15s;position:absolute;top:50%;left:4px;transform:translateY(-50%)}
|
||||||
|
.NB-splash-dropdown-divider{background:#00000014;height:1px;margin:6px 10px}.NB-splash-dropdown-grid{flex-wrap:wrap;gap:0;display:flex}a.NB-splash-dropdown-grid-item,a.NB-splash-dropdown-grid-item:link,a.NB-splash-dropdown-grid-item:visited,.NB-splash-dropdown-grid-item{color:#1a1508;box-sizing:border-box;border-radius:6px;flex:0 0 50%;padding:4px 10px;font-family:Gotham Narrow A,Gotham Narrow B,sans-serif;font-size:12px;font-weight:600;text-decoration:none;transition:color .15s,background .15s;display:block}
|
||||||
|
.NB-static-feature-training .NB-splash-dropdown-item-training,.NB-static-feature-ask-ai .NB-splash-dropdown-item-ask-ai,.NB-static-feature-web-feeds .NB-splash-dropdown-item-web-feeds,.NB-static-feature-newsletters .NB-splash-dropdown-item-newsletters,.NB-static-feature-search .NB-splash-dropdown-item-search,.NB-static-feature-archive .NB-splash-dropdown-item-archive,.NB-static-feature-saved .NB-splash-dropdown-item-saved,.NB-static-feature-native-apps .NB-splash-dropdown-item-native-apps{background:#0000000d}.NB-static-feature-training .NB-splash-dropdown-item-training .NB-splash-dropdown-item-name,.NB-static-feature-ask-ai .NB-splash-dropdown-item-ask-ai .NB-splash-dropdown-item-name,.NB-static-feature-web-feeds .NB-splash-dropdown-item-web-feeds .NB-splash-dropdown-item-name,.NB-static-feature-newsletters .NB-splash-dropdown-item-newsletters .NB-splash-dropdown-item-name,.NB-static-feature-search .NB-splash-dropdown-item-search .NB-splash-dropdown-item-name,.NB-static-feature-archive .NB-splash-dropdown-item-archive .NB-splash-dropdown-item-name,.NB-static-feature-saved .NB-splash-dropdown-item-saved .NB-splash-dropdown-item-name,.NB-static-feature-native-apps .NB-splash-dropdown-item-native-apps .NB-splash-dropdown-item-name{color:#1a1508}
|
||||||
|
.NB-splash-dropdown{max-width:calc(100vw - 32px)}
|
||||||
|
.NB-feature-detail-container{max-width:900px;margin:0 auto}.NB-feature-detail-hero{text-align:center;margin:0 0 48px}.NB-feature-detail-title{color:#2d261a;margin:12px 0 16px;font-size:42px;font-weight:700;line-height:1.1}.NB-feature-detail-subtitle{color:#2d261a99;max-width:600px;margin:0 auto;font-size:18px;line-height:1.5}.NB-feature-detail-screenshots{grid-template-columns:1fr 1fr;gap:24px;margin:0 0 48px;display:grid}
|
||||||
|
.NB-feature-detail-screenshots.NB-feature-detail-screenshots-single{grid-template-columns:1fr;max-width:700px;margin:0 auto 48px}.NB-feature-detail-screenshot{text-align:center}.NB-feature-detail-screenshot img{border:1px solid #0000001a;border-radius:8px;width:100%;box-shadow:0 4px 16px #0000001f}.NB-feature-detail-caption{color:#2d261a80;margin:10px 0 0;font-size:13px;line-height:1.4}.NB-feature-detail-how{margin:0 0 48px}.NB-feature-detail-how h2,.NB-feature-detail-faq h2{color:#2d261a;margin:0 0 20px;font-size:24px;font-weight:700}.NB-feature-detail-steps{counter-reset:step-counter;margin:0;padding:0;list-style:none}.NB-static .NB-feature-detail-step{counter-increment:step-counter;color:#2d261abf;border-bottom:1px solid #0000000f;padding:16px 0 16px 48px;font-size:18px;line-height:1.5;list-style:none;position:relative}.NB-feature-detail-step:last-child{border-bottom:none}.NB-feature-detail-step:before{content:counter(step-counter);color:#8a6d1b;background:#8a6d1b26;border-radius:50%;justify-content:center;align-items:center;width:30px;height:30px;font-size:14px;font-weight:700;display:flex;position:absolute;top:16px;left:0}.NB-feature-detail-faq{margin:0 0 48px}.NB-feature-detail-faq-item{border-bottom:1px solid #0000000f;padding:16px 0}.NB-feature-detail-faq-item:last-child{border-bottom:none}.NB-feature-detail-faq-q{color:#2d261ad9;margin:0 0 8px;font-size:18px;font-weight:600}.NB-feature-detail-faq-a{color:#2d261a8c;margin:0;font-size:16px;line-height:1.6}.NB-feature-detail-cta{text-align:center;flex-wrap:wrap;justify-content:center;gap:20px;padding:40px 0;display:flex}.NB-feature-detail-cta a{border-radius:10px;padding:14px 36px;font-family:Gotham Narrow A,Gotham Narrow B,sans-serif;font-size:16px;font-weight:600;text-decoration:none;transition:background .15s,color .15s,transform .15s,box-shadow .15s;display:inline-block}
|
||||||
|
.NB-static .NB-feature-detail-cta-primary{color:#2a2a1e;background:linear-gradient(135deg,#fbdb9b 0%,#f0c96e 100%);box-shadow:0 2px 8px #fbdb9b4d}
|
||||||
|
.NB-static .NB-feature-detail-cta-secondary{color:#3a3122;background:#fff6}
|
||||||
|
.NB-feature-detail-crosslinks{margin:0 0 32px}.NB-feature-detail-crosslinks h2{color:#2d261a;margin:0 0 20px;font-size:28px;font-weight:700}.NB-feature-detail-crosslinks-grid{grid-template-columns:1fr 1fr 1fr 1fr;gap:8px;display:grid}.NB-feature-detail-crosslink{background:#ffffff40;border-radius:8px;padding:14px 16px;text-decoration:none;transition:background .15s;display:block}
|
||||||
|
.NB-feature-detail-crosslink-name{color:#2d261ad9;margin:0 0 4px;font-family:Gotham Narrow A,Gotham Narrow B,sans-serif;font-size:15px;font-weight:600}.NB-feature-detail-crosslink-desc{color:#2d261a80;font-size:13px;line-height:1.4}.NB-static-feature-training .NB-feature-detail-crosslink-training,.NB-static-feature-ask-ai .NB-feature-detail-crosslink-ask-ai,.NB-static-feature-web-feeds .NB-feature-detail-crosslink-web-feeds,.NB-static-feature-newsletters .NB-feature-detail-crosslink-newsletters,.NB-static-feature-search .NB-feature-detail-crosslink-search,.NB-static-feature-archive .NB-feature-detail-crosslink-archive,.NB-static-feature-saved .NB-feature-detail-crosslink-saved,.NB-static-feature-native-apps .NB-feature-detail-crosslink-native-apps,.NB-static-feature-mcp .NB-feature-detail-crosslink-mcp,.NB-static-feature-cli .NB-feature-detail-crosslink-cli{background:#ffffff80}.NB-static-feature-training .NB-feature-detail-crosslink-training .NB-feature-detail-crosslink-name,.NB-static-feature-ask-ai .NB-feature-detail-crosslink-ask-ai .NB-feature-detail-crosslink-name,.NB-static-feature-web-feeds .NB-feature-detail-crosslink-web-feeds .NB-feature-detail-crosslink-name,.NB-static-feature-newsletters .NB-feature-detail-crosslink-newsletters .NB-feature-detail-crosslink-name,.NB-static-feature-search .NB-feature-detail-crosslink-search .NB-feature-detail-crosslink-name,.NB-static-feature-archive .NB-feature-detail-crosslink-archive .NB-feature-detail-crosslink-name,.NB-static-feature-saved .NB-feature-detail-crosslink-saved .NB-feature-detail-crosslink-name,.NB-static-feature-native-apps .NB-feature-detail-crosslink-native-apps .NB-feature-detail-crosslink-name,.NB-static-feature-mcp .NB-feature-detail-crosslink-mcp .NB-feature-detail-crosslink-name,.NB-static-feature-cli .NB-feature-detail-crosslink-cli .NB-feature-detail-crosslink-name{color:#8a6d1b}
|
||||||
|
.NB-feature-section-badge{text-transform:uppercase;letter-spacing:.5px;border-radius:4px;margin:0 0 12px;padding:3px 10px;font-size:11px;font-weight:700;display:inline-block}
|
||||||
|
.NB-feature-badge-archive{color:#0d7a6e;background:#0d8a7c1f}
|
||||||
|
.NB-theme-toggle{display:none}.NB-static .NB-theme-toggle,.NB-welcome .NB-theme-toggle{cursor:pointer;flex-shrink:0;align-items:center;gap:7px;display:flex}.NB-theme-toggle-sun,.NB-theme-toggle-moon{color:#0003;flex-shrink:0;transition:color .3s;display:block}
|
||||||
|
body.NB-dark .NB-theme-toggle-sun,body.NB-dark .NB-theme-toggle-moon{color:#ffffff40}body.NB-dark .NB-theme-toggle-moon{color:#8bb8d8}.NB-theme-toggle-track{cursor:pointer;appearance:none;background:#00000014;border:1px solid #00000026;border-radius:10px;outline:none;flex-shrink:0;width:36px;height:20px;padding:0;transition:background .2s,border-color .2s;position:relative}
|
||||||
|
body.NB-dark .NB-theme-toggle-track{background:#ffffff14;border-color:#ffffff26}
|
||||||
|
.NB-theme-toggle-thumb{background:#555;border-radius:50%;width:14px;height:14px;transition:transform .3s cubic-bezier(.4,0,.2,1),background-color .3s;display:block;position:absolute;top:2px;left:2px;box-shadow:0 1px 3px #0003}body.NB-dark .NB-theme-toggle-thumb{background:#8bb8d8;transform:translate(16px)}body.NB-static{background-color:#ebe0c2;background-image:linear-gradient(#ffffff12 0%,#ffffff08 20%,#0000 40%,#00000005 60%,#0000000a 80%,#0000000f 100%);transition:background-color .4s,color .4s}body.NB-static h1,body.NB-static h2,body.NB-static h3,body.NB-static h4{transition:color .4s}body.NB-static a{transition:color .3s}
|
||||||
|
body.NB-dark.NB-static .NB-splash-info{background-color:#488480e0;border-bottom-color:#ffffff26}body.NB-dark .NB-splash-top-link a{color:#ffffff80}
|
||||||
|
body.NB-dark.NB-static-features .NB-splash-top-link-features>a,body.NB-dark.NB-static-pricing .NB-splash-top-link-pricing>a,body.NB-dark.NB-static-about .NB-splash-top-link-about>a,body.NB-dark.NB-static-faq .NB-splash-top-link-faq>a,body.NB-dark.NB-static-compare .NB-splash-top-link-features>a{color:#fbdb9b;border-color:#fbdb9b4d}body.NB-dark.NB-static .NB-splash-info.NB-splash-bottom{background:#488480e6;border-top-color:#ffffff26}body.NB-dark.NB-static,body.NB-dark.NB-welcome,body.NB-dark.NB-welcome #NB-splash{color:silver;background-color:#2a524e;background-image:linear-gradient(#ffffff0a 0%,#ffffff05 20%,#0000 40%,#00000005 60%,#0000000a 80%,#0000000f 100%)}body.NB-dark.NB-static h1,body.NB-dark.NB-static h2,body.NB-dark.NB-static h3,body.NB-dark.NB-static h4{color:#e0e0e0}body.NB-dark.NB-static a{color:#fbdb9b}
|
||||||
|
.NB-dark .NB-splash-dropdown-inner{background:#10141cf2;border-color:#ffffff1f;box-shadow:0 12px 40px #0006,0 0 0 1px #ffffff0d}.NB-dark .NB-splash-dropdown-heading{color:#ffffff59}.NB-dark .NB-splash-dropdown-heading-link{color:#fbdb9b99}
|
||||||
|
.NB-dark .NB-splash-dropdown-item-name{color:#ffffffe6}
|
||||||
|
.NB-dark .NB-splash-dropdown-item-desc{color:#fff6}.NB-dark .NB-splash-dropdown-price{color:#fbdb9b}.NB-dark .NB-splash-dropdown-divider{background:#ffffff14}.NB-dark .NB-splash-dropdown-grid-item{color:#ffffff8c}
|
||||||
|
body.NB-dark.NB-static-feature-training .NB-splash-dropdown-item-training,body.NB-dark.NB-static-feature-ask-ai .NB-splash-dropdown-item-ask-ai,body.NB-dark.NB-static-feature-web-feeds .NB-splash-dropdown-item-web-feeds,body.NB-dark.NB-static-feature-newsletters .NB-splash-dropdown-item-newsletters,body.NB-dark.NB-static-feature-search .NB-splash-dropdown-item-search,body.NB-dark.NB-static-feature-archive .NB-splash-dropdown-item-archive,body.NB-dark.NB-static-feature-saved .NB-splash-dropdown-item-saved,body.NB-dark.NB-static-feature-native-apps .NB-splash-dropdown-item-native-apps{background:#ffffff0f}body.NB-dark.NB-static-feature-training .NB-splash-dropdown-item-training .NB-splash-dropdown-item-name,body.NB-dark.NB-static-feature-ask-ai .NB-splash-dropdown-item-ask-ai .NB-splash-dropdown-item-name,body.NB-dark.NB-static-feature-web-feeds .NB-splash-dropdown-item-web-feeds .NB-splash-dropdown-item-name,body.NB-dark.NB-static-feature-newsletters .NB-splash-dropdown-item-newsletters .NB-splash-dropdown-item-name,body.NB-dark.NB-static-feature-search .NB-splash-dropdown-item-search .NB-splash-dropdown-item-name,body.NB-dark.NB-static-feature-archive .NB-splash-dropdown-item-archive .NB-splash-dropdown-item-name,body.NB-dark.NB-static-feature-saved .NB-splash-dropdown-item-saved .NB-splash-dropdown-item-name,body.NB-dark.NB-static-feature-native-apps .NB-splash-dropdown-item-native-apps .NB-splash-dropdown-item-name{color:#fbdb9b}
|
||||||
|
.NB-dark .NB-feature-badge-archive{color:#fbbf24;background:#d9770633}
|
||||||
|
.NB-dark .NB-feature-detail-title{color:#fffffff2}.NB-dark .NB-feature-detail-subtitle{color:#ffffff8c}.NB-dark .NB-feature-detail-caption{color:#fff6}.NB-dark .NB-feature-detail-how h2,.NB-dark .NB-feature-detail-faq h2{color:#ffffffe6}.NB-dark .NB-feature-detail-step{color:#ffffffb3;border-bottom-color:#ffffff0f}.NB-dark .NB-feature-detail-step:before{color:#fbdb9b;background:#fbdb9b26}.NB-dark .NB-feature-detail-faq-item{border-bottom-color:#ffffff0f}.NB-dark .NB-feature-detail-faq-q{color:#ffffffd9}.NB-dark .NB-feature-detail-faq-a{color:#ffffff80}.NB-dark .NB-feature-detail-cta-primary{background:linear-gradient(135deg,#fbdb9b 0%,#f0c96e 100%);color:#2a2a1e!important}
|
||||||
|
.NB-dark .NB-feature-detail-cta-secondary{color:#fffc;background:#ffffff14}
|
||||||
|
.NB-dark .NB-feature-detail-crosslinks h2{color:#ffffffe6}.NB-dark .NB-feature-detail-crosslink{background:#ffffff0f}
|
||||||
|
.NB-dark .NB-feature-detail-crosslink-name{color:#ffffffd9}.NB-dark .NB-feature-detail-crosslink-desc{color:#ffffff73}.NB-dark.NB-static-feature-training .NB-feature-detail-crosslink-training,.NB-dark.NB-static-feature-ask-ai .NB-feature-detail-crosslink-ask-ai,.NB-dark.NB-static-feature-web-feeds .NB-feature-detail-crosslink-web-feeds,.NB-dark.NB-static-feature-newsletters .NB-feature-detail-crosslink-newsletters,.NB-dark.NB-static-feature-search .NB-feature-detail-crosslink-search,.NB-dark.NB-static-feature-archive .NB-feature-detail-crosslink-archive,.NB-dark.NB-static-feature-saved .NB-feature-detail-crosslink-saved,.NB-dark.NB-static-feature-native-apps .NB-feature-detail-crosslink-native-apps,.NB-dark.NB-static-feature-mcp .NB-feature-detail-crosslink-mcp,.NB-dark.NB-static-feature-cli .NB-feature-detail-crosslink-cli{background:#fbdb9b1f}.NB-dark.NB-static-feature-training .NB-feature-detail-crosslink-training .NB-feature-detail-crosslink-name,.NB-dark.NB-static-feature-ask-ai .NB-feature-detail-crosslink-ask-ai .NB-feature-detail-crosslink-name,.NB-dark.NB-static-feature-web-feeds .NB-feature-detail-crosslink-web-feeds .NB-feature-detail-crosslink-name,.NB-dark.NB-static-feature-newsletters .NB-feature-detail-crosslink-newsletters .NB-feature-detail-crosslink-name,.NB-dark.NB-static-feature-search .NB-feature-detail-crosslink-search .NB-feature-detail-crosslink-name,.NB-dark.NB-static-feature-archive .NB-feature-detail-crosslink-archive .NB-feature-detail-crosslink-name,.NB-dark.NB-static-feature-saved .NB-feature-detail-crosslink-saved .NB-feature-detail-crosslink-name,.NB-dark.NB-static-feature-native-apps .NB-feature-detail-crosslink-native-apps .NB-feature-detail-crosslink-name,.NB-dark.NB-static-feature-mcp .NB-feature-detail-crosslink-mcp .NB-feature-detail-crosslink-name,.NB-dark.NB-static-feature-cli .NB-feature-detail-crosslink-cli .NB-feature-detail-crosslink-name{color:#fbdb9b}
|
||||||
|
.NB-dark .NB-feature-detail-screenshot{border-color:#ffffff14}.NB-dark .NB-feature-detail-screenshot img{border-color:#ffffff0f}
|
||||||
|
body.NB-dark.NB-static li{color:#ffffffbf}
|
||||||
|
body.NB-dark{color:silver;background-color:#191b1c}
|
||||||
|
.NB-dark a:-webkit-any-link{color:#83b4e0}
|
||||||
|
.NB-dark{scrollbar-color:#565859 #262829}
|
||||||
|
.NB-dark .NB-splash-info .NB-splash-links a{color:#83b4e0;text-shadow:none}
|
||||||
|
.NB-dark .NB-splash-info .NB-splash-links a,.NB-dark .NB-static a,.NB-dark .NB-splash-link{color:#fbdb9b}
|
||||||
|
.NB-dark .NB-feeds-header-user,.NB-dark .NB-feedbar,.NB-dark .NB-taskbar-container,.NB-dark #story_titles .NB-feedbar,.NB-dark .NB-splash-info.NB-splash-bottom,.NB-dark .left-south{color:#ddd;text-shadow:0 1px #000c;background:#393b3c;border-color:#303030}
|
||||||
|
.NB-dark .NB-splash-blurred-logo{background:url(/media/embed/logo_newsblur_blur_dark.png) 50%/contain no-repeat}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="NB-static NB-static-features NB-static-feature-archive NB-dark">
|
||||||
|
|
||||||
|
|
||||||
|
<div class="NB-body-inner">
|
||||||
|
<div class="NB-splash-info NB-splash-top">
|
||||||
|
<div class="NB-splash-top-nav">
|
||||||
|
<span class="NB-splash-top-title">Story Archive</span>
|
||||||
|
<ul class="NB-splash-top-links">
|
||||||
|
<li class="NB-splash-top-link NB-splash-top-link-home"><a href="https://newsblur.com/">Home</a></li>
|
||||||
|
<li class="NB-splash-top-link NB-splash-top-link-features NB-splash-nav-has-dropdown" data-dropdown-key="dropdown-tyjeaup">
|
||||||
|
<a href="https://newsblur.com/features">Features</a>
|
||||||
|
|
||||||
|
</li>
|
||||||
|
<li class="NB-splash-top-link NB-splash-top-link-pricing NB-splash-nav-has-dropdown" data-dropdown-key="dropdown-h1spa4p">
|
||||||
|
<a href="https://newsblur.com/pricing">Pricing</a>
|
||||||
|
|
||||||
|
</li>
|
||||||
|
<li class="NB-splash-top-link NB-splash-top-link-faq"><a href="https://newsblur.com/faq">FAQ</a></li>
|
||||||
|
<li class="NB-splash-top-link NB-splash-top-link-about"><a href="https://newsblur.com/about">About</a></li>
|
||||||
|
</ul>
|
||||||
|
<div class="NB-splash-top-logo">
|
||||||
|
<div class="NB-theme-toggle" title="Toggle theme">
|
||||||
|
<svg class="NB-theme-toggle-sun" viewBox="0 0 24 24" width="14" height="14" fill="none">
|
||||||
|
<circle cx="12" cy="12" r="4" fill="currentColor"></circle>
|
||||||
|
<path d="M12 2v3M12 19v3M4.22 4.22l2.12 2.12M17.66 17.66l2.12 2.12M2 12h3M19 12h3M4.22 19.78l2.12-2.12M17.66 6.34l2.12-2.12" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path>
|
||||||
|
</svg>
|
||||||
|
<button class="NB-theme-toggle-track" aria-label="Toggle dark mode" type="button">
|
||||||
|
<span class="NB-theme-toggle-thumb"></span>
|
||||||
|
</button>
|
||||||
|
<svg class="NB-theme-toggle-moon" viewBox="0 0 24 24" width="14" height="14" fill="none">
|
||||||
|
<path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z" fill="currentColor"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<a href="https://newsblur.com/">
|
||||||
|
<div class="NB-splash-blurred-logo"></div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="https://newsblur.com/" class="NB-splash-top-logo-fallback">
|
||||||
|
<div class="NB-splash-title NB-splash-blurred-logo"></div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<a href="https://newsblur.com/" class="NB-mobile-logo-link">
|
||||||
|
<div class="NB-mobile-logo"></div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="NB-feature-detail-container">
|
||||||
|
|
||||||
|
<div class="NB-feature-detail-hero">
|
||||||
|
<div class="NB-feature-section-badge NB-feature-badge-archive">Archive</div>
|
||||||
|
<h1 class="NB-feature-detail-title">Story Archive</h1>
|
||||||
|
<p class="NB-feature-detail-subtitle">Every story archived and searchable forever. Your personal knowledge base.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="NB-feature-detail-screenshots NB-feature-detail-screenshots-single">
|
||||||
|
<div class="NB-feature-detail-screenshot">
|
||||||
|
<img src="./story_archive_newsblur_files/archive.png" alt="Archive showing unread stories going back months">
|
||||||
|
<div class="NB-feature-detail-caption">Archive showing unread stories going back months</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="NB-feature-detail-how">
|
||||||
|
<h2>How it works</h2>
|
||||||
|
<ol class="NB-feature-detail-steps">
|
||||||
|
<li class="NB-feature-detail-step">Subscribe to Premium Archive</li>
|
||||||
|
<li class="NB-feature-detail-step">Every new story is archived permanently</li>
|
||||||
|
<li class="NB-feature-detail-step">When you subscribe to a new feed, its history is back-filled</li>
|
||||||
|
<li class="NB-feature-detail-step">Stories never expire or disappear</li>
|
||||||
|
<li class="NB-feature-detail-step">Search your entire archive with full-text search</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="NB-feature-detail-faq">
|
||||||
|
<div class="NB-feature-detail-faq-item">
|
||||||
|
<h3 class="NB-feature-detail-faq-q">How far back does the archive go?</h3>
|
||||||
|
<p class="NB-feature-detail-faq-a">When you subscribe to a feed, NewsBlur back-fills its entire available history. Going forward, every story is kept forever.</p>
|
||||||
|
</div>
|
||||||
|
<div class="NB-feature-detail-faq-item">
|
||||||
|
<h3 class="NB-feature-detail-faq-q">What happens if I downgrade from Archive?</h3>
|
||||||
|
<p class="NB-feature-detail-faq-a">Your archived stories remain accessible, but new stories will start following the standard retention policy.</p>
|
||||||
|
</div>
|
||||||
|
<div class="NB-feature-detail-faq-item">
|
||||||
|
<h3 class="NB-feature-detail-faq-q">Can I search the archive?</h3>
|
||||||
|
<p class="NB-feature-detail-faq-a">Yes, every archived story is fully searchable with Elasticsearch.</p>
|
||||||
|
</div>
|
||||||
|
<div class="NB-feature-detail-faq-item">
|
||||||
|
<h3 class="NB-feature-detail-faq-q">What plan do I need?</h3>
|
||||||
|
<p class="NB-feature-detail-faq-a">Story Archive requires Premium Archive ($99/year) or Premium Pro ($29/month).</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="NB-feature-detail-crosslinks">
|
||||||
|
<h2>Explore more features</h2>
|
||||||
|
<div class="NB-feature-detail-crosslinks-grid">
|
||||||
|
<a href="https://newsblur.com/features/intelligence-training" class="NB-feature-detail-crosslink NB-feature-detail-crosslink-training">
|
||||||
|
<div class="NB-feature-detail-crosslink-name"><img src="./story_archive_newsblur_files/train.svg" class="NB-splash-dropdown-icon NB-splash-dropdown-icon-blue">Intelligence Training</div>
|
||||||
|
<div class="NB-feature-detail-crosslink-desc">Train by author, tag, title, and full text</div>
|
||||||
|
</a>
|
||||||
|
<a href="https://newsblur.com/features/ask-ai" class="NB-feature-detail-crosslink NB-feature-detail-crosslink-ask-ai">
|
||||||
|
<div class="NB-feature-detail-crosslink-name"><img src="./story_archive_newsblur_files/prompt.svg" class="NB-splash-dropdown-icon NB-splash-dropdown-icon-coral">Ask AI & Daily Briefing</div>
|
||||||
|
<div class="NB-feature-detail-crosslink-desc">AI-powered story analysis and daily summaries</div>
|
||||||
|
</a>
|
||||||
|
<a href="https://newsblur.com/features/web-feeds" class="NB-feature-detail-crosslink NB-feature-detail-crosslink-web-feeds">
|
||||||
|
<div class="NB-feature-detail-crosslink-name"><img src="./story_archive_newsblur_files/web-feed.svg" class="NB-splash-dropdown-icon NB-splash-dropdown-icon-purple">Web Feeds</div>
|
||||||
|
<div class="NB-feature-detail-crosslink-desc">Follow any website, even without RSS</div>
|
||||||
|
</a>
|
||||||
|
<a href="https://newsblur.com/features/newsletters" class="NB-feature-detail-crosslink NB-feature-detail-crosslink-newsletters">
|
||||||
|
<div class="NB-feature-detail-crosslink-name"><img src="./story_archive_newsblur_files/email.svg" class="NB-splash-dropdown-icon NB-splash-dropdown-icon-pink">Email Newsletters</div>
|
||||||
|
<div class="NB-feature-detail-crosslink-desc">Read newsletters in your news reader</div>
|
||||||
|
</a>
|
||||||
|
<a href="https://newsblur.com/features/search" class="NB-feature-detail-crosslink NB-feature-detail-crosslink-search">
|
||||||
|
<div class="NB-feature-detail-crosslink-name"><img src="./story_archive_newsblur_files/search.svg" class="NB-splash-dropdown-icon NB-splash-dropdown-icon-teal">Full-Text Search</div>
|
||||||
|
<div class="NB-feature-detail-crosslink-desc">Search across all your subscriptions</div>
|
||||||
|
</a>
|
||||||
|
<a href="https://newsblur.com/features/archive" class="NB-feature-detail-crosslink NB-feature-detail-crosslink-archive">
|
||||||
|
<div class="NB-feature-detail-crosslink-name"><img src="./story_archive_newsblur_files/archive.svg" class="NB-splash-dropdown-icon NB-splash-dropdown-icon-orange">Story Archive</div>
|
||||||
|
<div class="NB-feature-detail-crosslink-desc">Every story kept and searchable forever</div>
|
||||||
|
</a>
|
||||||
|
<a href="https://newsblur.com/features/saved-stories" class="NB-feature-detail-crosslink NB-feature-detail-crosslink-saved">
|
||||||
|
<div class="NB-feature-detail-crosslink-name"><img src="./story_archive_newsblur_files/tag.svg" class="NB-splash-dropdown-icon NB-splash-dropdown-icon-green">Saved Stories & Tags</div>
|
||||||
|
<div class="NB-feature-detail-crosslink-desc">Save stories and organize with custom tags</div>
|
||||||
|
</a>
|
||||||
|
<a href="https://newsblur.com/features/native-apps" class="NB-feature-detail-crosslink NB-feature-detail-crosslink-native-apps">
|
||||||
|
<div class="NB-feature-detail-crosslink-name"><img src="./story_archive_newsblur_files/dialog-goodies.svg" class="NB-splash-dropdown-icon NB-splash-dropdown-icon-gold">Native Apps</div>
|
||||||
|
<div class="NB-feature-detail-crosslink-desc">First-class iOS & Android apps, not wrappers</div>
|
||||||
|
</a>
|
||||||
|
<a href="https://newsblur.com/features/mcp" class="NB-feature-detail-crosslink NB-feature-detail-crosslink-mcp">
|
||||||
|
<div class="NB-feature-detail-crosslink-name"><img src="./story_archive_newsblur_files/mcp-agent.svg" class="NB-splash-dropdown-icon NB-splash-dropdown-icon-indigo">MCP Server</div>
|
||||||
|
<div class="NB-feature-detail-crosslink-desc">Connect AI agents to your feeds and stories</div>
|
||||||
|
</a>
|
||||||
|
<a href="https://newsblur.com/features/cli" class="NB-feature-detail-crosslink NB-feature-detail-crosslink-cli">
|
||||||
|
<div class="NB-feature-detail-crosslink-name"><img src="./story_archive_newsblur_files/cli-terminal.svg" class="NB-splash-dropdown-icon NB-splash-dropdown-icon-cyan">CLI Tool</div>
|
||||||
|
<div class="NB-feature-detail-crosslink-desc">Manage feeds and stories from your terminal</div>
|
||||||
|
</a>
|
||||||
|
<a href="https://newsblur.com/features/story-clustering" class="NB-feature-detail-crosslink NB-feature-detail-crosslink-clustering">
|
||||||
|
<div class="NB-feature-detail-crosslink-name"><img src="./story_archive_newsblur_files/stack.svg" class="NB-splash-dropdown-icon NB-splash-dropdown-icon-indigo">Story Clustering</div>
|
||||||
|
<div class="NB-feature-detail-crosslink-desc">Group duplicate stories across your feeds</div>
|
||||||
|
</a>
|
||||||
|
<a href="https://newsblur.com/features#integrations" class="NB-feature-detail-crosslink NB-feature-detail-crosslink-integrations">
|
||||||
|
<div class="NB-feature-detail-crosslink-name"><img src="./story_archive_newsblur_files/link.svg" class="NB-splash-dropdown-icon NB-splash-dropdown-icon-cyan">IFTTT & Zapier</div>
|
||||||
|
<div class="NB-feature-detail-crosslink-desc">Automate workflows with hundreds of services</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="NB-feature-detail-cta">
|
||||||
|
<a href="https://newsblur.com/features" class="NB-feature-detail-cta-primary">See all features</a>
|
||||||
|
<a href="https://newsblur.com/" class="NB-feature-detail-cta-secondary">Try NewsBlur free</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="NB-splash-info NB-splash-bottom">
|
||||||
|
<ul class="NB-splash-links NB-inner">
|
||||||
|
<li class="NB-splash-link NB-splash-link-features NB-active"><a href="https://newsblur.com/features">Features</a></li>
|
||||||
|
<li class="NB-splash-link NB-splash-link-pricing "><a href="https://newsblur.com/pricing">Pricing</a></li>
|
||||||
|
<li class="NB-splash-link NB-splash-link-about "><a href="https://newsblur.com/about">About</a></li>
|
||||||
|
<li class="NB-splash-link NB-splash-link-faq "><a href="https://newsblur.com/faq">FAQ</a></li>
|
||||||
|
<li class="NB-splash-link NB-splash-link-api "><a href="https://newsblur.com/api">API</a></li>
|
||||||
|
<li class="NB-splash-link NB-splash-link-ios "><a href="https://newsblur.com/ios">iOS</a></li>
|
||||||
|
<li class="NB-splash-link NB-splash-link-android "><a href="https://newsblur.com/android">Android</a></li>
|
||||||
|
<li class="NB-splash-link NB-splash-link-github"><a href="https://github.com/samuelclay">GitHub</a></li>
|
||||||
|
<li class="NB-splash-link NB-splash-link-discourse"><a href="https://forum.newsblur.com/">Support</a></li>
|
||||||
|
<li class="NB-splash-link NB-splash-link-blog"><a href="https://blog.newsblur.com/">The Blog</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="NB-splash-link NB-splash-link-logo">
|
||||||
|
<a href="https://newsblur.com/"><div class="NB-splash-blurred-logo"></div></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="NB-splash-dropdown NB-splash-dropdown-features" data-dropdown-key="dropdown-tyjeaup">
|
||||||
|
<div class="NB-splash-dropdown-inner">
|
||||||
|
<div class="NB-splash-dropdown-col NB-splash-dropdown-col-hero">
|
||||||
|
<div class="NB-splash-dropdown-heading">
|
||||||
|
<a href="https://newsblur.com/features" class="NB-splash-dropdown-heading-link">All Features →</a>
|
||||||
|
</div>
|
||||||
|
<a href="https://newsblur.com/features/intelligence-training" class="NB-splash-dropdown-item NB-splash-dropdown-item-training">
|
||||||
|
<div class="NB-splash-dropdown-item-name"><img src="./story_archive_newsblur_files/train.svg" class="NB-splash-dropdown-icon NB-splash-dropdown-icon-blue">Intelligence Training</div>
|
||||||
|
<div class="NB-splash-dropdown-item-desc">Train by author, tag, title, and full text</div>
|
||||||
|
</a>
|
||||||
|
<a href="https://newsblur.com/features/ask-ai" class="NB-splash-dropdown-item NB-splash-dropdown-item-ask-ai">
|
||||||
|
<div class="NB-splash-dropdown-item-name"><img src="./story_archive_newsblur_files/prompt.svg" class="NB-splash-dropdown-icon NB-splash-dropdown-icon-coral">Ask AI & Daily Briefing</div>
|
||||||
|
<div class="NB-splash-dropdown-item-desc">AI-powered story analysis and daily summaries</div>
|
||||||
|
</a>
|
||||||
|
<a href="https://newsblur.com/features/web-feeds" class="NB-splash-dropdown-item NB-splash-dropdown-item-web-feeds">
|
||||||
|
<div class="NB-splash-dropdown-item-name"><img src="./story_archive_newsblur_files/web-feed.svg" class="NB-splash-dropdown-icon NB-splash-dropdown-icon-purple">Web Feeds</div>
|
||||||
|
<div class="NB-splash-dropdown-item-desc">Follow any website, even without RSS</div>
|
||||||
|
</a>
|
||||||
|
<a href="https://newsblur.com/features/newsletters" class="NB-splash-dropdown-item NB-splash-dropdown-item-newsletters">
|
||||||
|
<div class="NB-splash-dropdown-item-name"><img src="./story_archive_newsblur_files/email.svg" class="NB-splash-dropdown-icon NB-splash-dropdown-icon-pink">Email Newsletters</div>
|
||||||
|
<div class="NB-splash-dropdown-item-desc">Read newsletters in your news reader</div>
|
||||||
|
</a>
|
||||||
|
<a href="https://newsblur.com/features/search" class="NB-splash-dropdown-item NB-splash-dropdown-item-search">
|
||||||
|
<div class="NB-splash-dropdown-item-name"><img src="./story_archive_newsblur_files/search.svg" class="NB-splash-dropdown-icon NB-splash-dropdown-icon-teal">Full-Text Search</div>
|
||||||
|
<div class="NB-splash-dropdown-item-desc">Search across all your subscriptions</div>
|
||||||
|
</a>
|
||||||
|
<a href="https://newsblur.com/features/archive" class="NB-splash-dropdown-item NB-splash-dropdown-item-archive">
|
||||||
|
<div class="NB-splash-dropdown-item-name"><img src="./story_archive_newsblur_files/archive.svg" class="NB-splash-dropdown-icon NB-splash-dropdown-icon-orange">Story Archive</div>
|
||||||
|
<div class="NB-splash-dropdown-item-desc">Every story kept and searchable forever</div>
|
||||||
|
</a>
|
||||||
|
<a href="https://newsblur.com/features/saved-stories" class="NB-splash-dropdown-item NB-splash-dropdown-item-saved">
|
||||||
|
<div class="NB-splash-dropdown-item-name"><img src="./story_archive_newsblur_files/tag.svg" class="NB-splash-dropdown-icon NB-splash-dropdown-icon-green">Saved Stories & Tags</div>
|
||||||
|
<div class="NB-splash-dropdown-item-desc">Save stories and organize with custom tags</div>
|
||||||
|
</a>
|
||||||
|
<a href="https://newsblur.com/features/native-apps" class="NB-splash-dropdown-item NB-splash-dropdown-item-native-apps">
|
||||||
|
<div class="NB-splash-dropdown-item-name"><img src="./story_archive_newsblur_files/dialog-goodies.svg" class="NB-splash-dropdown-icon NB-splash-dropdown-icon-gold">Native Apps</div>
|
||||||
|
<div class="NB-splash-dropdown-item-desc">First-class iOS & Android apps, not wrappers</div>
|
||||||
|
</a>
|
||||||
|
<a href="https://newsblur.com/features/story-clustering" class="NB-splash-dropdown-item NB-splash-dropdown-item-clustering">
|
||||||
|
<div class="NB-splash-dropdown-item-name"><img src="./story_archive_newsblur_files/stack.svg" class="NB-splash-dropdown-icon NB-splash-dropdown-icon-indigo">Story Clustering</div>
|
||||||
|
<div class="NB-splash-dropdown-item-desc">Group duplicate stories across your feeds</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="NB-splash-dropdown-col NB-splash-dropdown-col-more">
|
||||||
|
<div class="NB-splash-dropdown-heading">More Features</div>
|
||||||
|
<div class="NB-splash-dropdown-grid">
|
||||||
|
<a href="https://newsblur.com/features#river" class="NB-splash-dropdown-grid-item">River of News</a>
|
||||||
|
<a href="https://newsblur.com/features#notifications" class="NB-splash-dropdown-grid-item">Notifications</a>
|
||||||
|
<a href="https://newsblur.com/features#text-view" class="NB-splash-dropdown-grid-item">Text View</a>
|
||||||
|
<a href="https://newsblur.com/features#changes" class="NB-splash-dropdown-grid-item">Track Changes</a>
|
||||||
|
<a href="https://newsblur.com/features#themes" class="NB-splash-dropdown-grid-item">Themes</a>
|
||||||
|
<a href="https://newsblur.com/features#shortcuts" class="NB-splash-dropdown-grid-item">Keyboard Shortcuts</a>
|
||||||
|
<a href="https://newsblur.com/features#integrations" class="NB-splash-dropdown-grid-item">IFTTT & Zapier</a>
|
||||||
|
<a href="https://newsblur.com/features#blurblog" class="NB-splash-dropdown-grid-item">Shared Stories</a>
|
||||||
|
<a href="https://newsblur.com/features/mcp" class="NB-splash-dropdown-grid-item">MCP Server</a>
|
||||||
|
<a href="https://newsblur.com/features/cli" class="NB-splash-dropdown-grid-item">CLI Tool</a>
|
||||||
|
</div>
|
||||||
|
<div class="NB-splash-dropdown-divider"></div>
|
||||||
|
<div class="NB-splash-dropdown-heading">Compare</div>
|
||||||
|
<div class="NB-splash-dropdown-grid">
|
||||||
|
<a href="https://newsblur.com/compare/feedly" class="NB-splash-dropdown-grid-item NB-splash-dropdown-grid-item-compare-feedly">vs Feedly</a>
|
||||||
|
<a href="https://newsblur.com/compare/inoreader" class="NB-splash-dropdown-grid-item NB-splash-dropdown-grid-item-compare-inoreader">vs Inoreader</a>
|
||||||
|
<a href="https://newsblur.com/compare/readwise-reader" class="NB-splash-dropdown-grid-item NB-splash-dropdown-grid-item-compare-readwise">vs Readwise</a>
|
||||||
|
<a href="https://newsblur.com/compare/the-old-reader" class="NB-splash-dropdown-grid-item NB-splash-dropdown-grid-item-compare-old-reader">vs Old Reader</a>
|
||||||
|
<a href="https://newsblur.com/compare/feedbin" class="NB-splash-dropdown-grid-item NB-splash-dropdown-grid-item-compare-feedbin">vs Feedbin</a>
|
||||||
|
</div>
|
||||||
|
<div class="NB-splash-dropdown-divider"></div>
|
||||||
|
<div class="NB-splash-dropdown-heading">Alternatives</div>
|
||||||
|
<div class="NB-splash-dropdown-grid">
|
||||||
|
<a href="https://newsblur.com/alternative/feedly" class="NB-splash-dropdown-grid-item NB-splash-dropdown-grid-item-alt-feedly">Feedly Alternative</a>
|
||||||
|
<a href="https://newsblur.com/alternative/google-reader" class="NB-splash-dropdown-grid-item NB-splash-dropdown-grid-item-alt-google-reader">Google Reader Alt</a>
|
||||||
|
<a href="https://newsblur.com/alternative/open-source-rss-reader" class="NB-splash-dropdown-grid-item NB-splash-dropdown-grid-item-alt-open-source">Open Source</a>
|
||||||
|
<a href="https://newsblur.com/alternative/inoreader" class="NB-splash-dropdown-grid-item NB-splash-dropdown-grid-item-alt-inoreader">Inoreader Alternative</a>
|
||||||
|
<a href="https://newsblur.com/alternative/self-hosted-rss-reader" class="NB-splash-dropdown-grid-item NB-splash-dropdown-grid-item-alt-self-hosted">Self-Hosted</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div><div class="NB-splash-dropdown NB-splash-dropdown-pricing" data-dropdown-key="dropdown-h1spa4p">
|
||||||
|
<div class="NB-splash-dropdown-inner">
|
||||||
|
<a href="https://newsblur.com/pricing" class="NB-splash-dropdown-item NB-splash-dropdown-item-allplans">
|
||||||
|
<div class="NB-splash-dropdown-item-name">All Plans</div>
|
||||||
|
<div class="NB-splash-dropdown-item-desc">Compare every tier side by side</div>
|
||||||
|
</a>
|
||||||
|
<div class="NB-splash-dropdown-divider"></div>
|
||||||
|
<a href="https://newsblur.com/pricing/premium" class="NB-splash-dropdown-item NB-splash-dropdown-tier NB-splash-dropdown-tier-premium">
|
||||||
|
<div class="NB-splash-dropdown-item-name">Premium <span class="NB-splash-dropdown-price">$36/yr</span></div>
|
||||||
|
<div class="NB-splash-dropdown-item-desc">1,024 sites, search, text view, river of news</div>
|
||||||
|
</a>
|
||||||
|
<a href="https://newsblur.com/pricing/archive" class="NB-splash-dropdown-item NB-splash-dropdown-tier NB-splash-dropdown-tier-archive">
|
||||||
|
<div class="NB-splash-dropdown-item-name">Premium Archive <span class="NB-splash-dropdown-price">$99/yr</span></div>
|
||||||
|
<div class="NB-splash-dropdown-item-desc">Full archive, Ask AI, daily briefing, 4,096 sites</div>
|
||||||
|
</a>
|
||||||
|
<a href="https://newsblur.com/pricing/pro" class="NB-splash-dropdown-item NB-splash-dropdown-tier NB-splash-dropdown-tier-pro">
|
||||||
|
<div class="NB-splash-dropdown-item-name">Premium Pro <span class="NB-splash-dropdown-price">$29/mo</span></div>
|
||||||
|
<div class="NB-splash-dropdown-item-desc">5-min fetching, regex training, 10,000 sites</div>
|
||||||
|
</a>
|
||||||
|
<div class="NB-splash-dropdown-divider"></div>
|
||||||
|
<a href="https://newsblur.com/faq" class="NB-splash-dropdown-item NB-splash-dropdown-item-faq">
|
||||||
|
<div class="NB-splash-dropdown-item-name">FAQ</div>
|
||||||
|
<div class="NB-splash-dropdown-item-desc">Common questions about plans and billing</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div></body></html>
|
||||||
@@ -306,6 +306,7 @@ enum class PropertyId : Uint32 {
|
|||||||
GridArea = String::hash( "grid-area" ),
|
GridArea = String::hash( "grid-area" ),
|
||||||
JustifyItems = String::hash( "justify-items" ),
|
JustifyItems = String::hash( "justify-items" ),
|
||||||
JustifySelf = String::hash( "justify-self" ),
|
JustifySelf = String::hash( "justify-self" ),
|
||||||
|
Defer = String::hash( "defer" ),
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class PropertyType : Uint32 {
|
enum class PropertyType : Uint32 {
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ enum class LayoutInvalidationReason : Uint32 {
|
|||||||
|
|
||||||
using LayoutInvalidationFlags = Uint32;
|
using LayoutInvalidationFlags = Uint32;
|
||||||
|
|
||||||
inline LayoutInvalidationFlags toLayoutInvalidationFlags( LayoutInvalidationReason reason ) {
|
inline constexpr LayoutInvalidationFlags
|
||||||
|
toLayoutInvalidationFlags( LayoutInvalidationReason reason ) {
|
||||||
return static_cast<LayoutInvalidationFlags>( reason );
|
return static_cast<LayoutInvalidationFlags>( reason );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
#ifndef EE_UI_UIIMAGE_HPP
|
#ifndef EE_UI_UIIMAGE_HPP
|
||||||
#define EE_UI_UIIMAGE_HPP
|
#define EE_UI_UIIMAGE_HPP
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
#include <eepp/network/uri.hpp>
|
#include <eepp/network/uri.hpp>
|
||||||
#include <eepp/ui/uiwidget.hpp>
|
#include <eepp/ui/uiwidget.hpp>
|
||||||
#include <atomic>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
namespace EE { namespace UI {
|
namespace EE { namespace UI {
|
||||||
@@ -59,6 +59,7 @@ class EE_API UIImage : public UIWidget {
|
|||||||
Vector2f mDestSize;
|
Vector2f mDestSize;
|
||||||
Uint32 mResourceChangeCb;
|
Uint32 mResourceChangeCb;
|
||||||
bool mDrawableOwner;
|
bool mDrawableOwner;
|
||||||
|
bool mDeferLoad{ false };
|
||||||
std::shared_ptr<std::atomic<bool>> mAsyncImageAlive;
|
std::shared_ptr<std::atomic<bool>> mAsyncImageAlive;
|
||||||
Uint64 mRemoteImageLoadId{ 0 };
|
Uint64 mRemoteImageLoadId{ 0 };
|
||||||
|
|
||||||
@@ -82,6 +83,8 @@ class EE_API UIImage : public UIWidget {
|
|||||||
|
|
||||||
void onDrawableResourceEvent( DrawableResource::Event event, DrawableResource* );
|
void onDrawableResourceEvent( DrawableResource::Event event, DrawableResource* );
|
||||||
|
|
||||||
|
bool loadFileDrawable( const Network::URI& uri );
|
||||||
|
|
||||||
void loadRemoteDrawable( const Network::URI& uri );
|
void loadRemoteDrawable( const Network::URI& uri );
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ class EE_API UILayout : public UIWidget {
|
|||||||
UnorderedSet<UILayout*> mLayouts;
|
UnorderedSet<UILayout*> mLayouts;
|
||||||
bool mDirtyLayout{ false };
|
bool mDirtyLayout{ false };
|
||||||
LayoutInvalidationFlags mDirtyReasons{ 0 };
|
LayoutInvalidationFlags mDirtyReasons{ 0 };
|
||||||
|
LayoutInvalidationFlags mCurrentLayoutReasons{ 0 };
|
||||||
// True only while updateLayoutTree() is actively updating this layout and walking its
|
// True only while updateLayoutTree() is actively updating this layout and walking its
|
||||||
// descendants. This is intentionally narrower than mDirtyLayout: code that receives layout
|
// descendants. This is intentionally narrower than mDirtyLayout: code that receives layout
|
||||||
// notifications can use it to distinguish "already scheduled for a future pass" from
|
// notifications can use it to distinguish "already scheduled for a future pass" from
|
||||||
|
|||||||
@@ -528,6 +528,7 @@ void StyleSheetSpecification::registerDefaultProperties() {
|
|||||||
registerProperty( "grid-area", "auto" ).setType( PropertyType::String );
|
registerProperty( "grid-area", "auto" ).setType( PropertyType::String );
|
||||||
registerProperty( "justify-items", "normal" ).setType( PropertyType::String );
|
registerProperty( "justify-items", "normal" ).setType( PropertyType::String );
|
||||||
registerProperty( "justify-self", "auto" ).setType( PropertyType::String );
|
registerProperty( "justify-self", "auto" ).setType( PropertyType::String );
|
||||||
|
registerProperty( "defer", "0" ).setType( PropertyType::Bool );
|
||||||
|
|
||||||
// Shorthands
|
// Shorthands
|
||||||
registerShorthand( "margin", { "margin-top", "margin-right", "margin-bottom", "margin-left" },
|
registerShorthand( "margin", { "margin-top", "margin-right", "margin-bottom", "margin-left" },
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include <eepp/ui/uiicon.hpp>
|
#include <eepp/ui/uiicon.hpp>
|
||||||
#include <eepp/ui/uiimage.hpp>
|
#include <eepp/ui/uiimage.hpp>
|
||||||
#include <eepp/ui/uiscenenode.hpp>
|
#include <eepp/ui/uiscenenode.hpp>
|
||||||
|
#include <eepp/window/engine.hpp>
|
||||||
|
|
||||||
namespace EE { namespace UI {
|
namespace EE { namespace UI {
|
||||||
|
|
||||||
@@ -56,6 +57,8 @@ UIImage* UIImage::setDrawable( Drawable* drawable, bool ownIt ) {
|
|||||||
if ( drawable == mDrawable )
|
if ( drawable == mDrawable )
|
||||||
return this;
|
return this;
|
||||||
|
|
||||||
|
Sizef oldSize( mSize );
|
||||||
|
|
||||||
safeDeleteDrawable();
|
safeDeleteDrawable();
|
||||||
|
|
||||||
mDrawable = drawable;
|
mDrawable = drawable;
|
||||||
@@ -87,6 +90,9 @@ UIImage* UIImage::setDrawable( Drawable* drawable, bool ownIt ) {
|
|||||||
|
|
||||||
onAutoSize();
|
onAutoSize();
|
||||||
|
|
||||||
|
if ( mSize != oldSize )
|
||||||
|
notifyLayoutAttrChangeParent( LayoutInvalidation::ParentReplacedFormatting );
|
||||||
|
|
||||||
autoAlign();
|
autoAlign();
|
||||||
|
|
||||||
invalidateDraw();
|
invalidateDraw();
|
||||||
@@ -293,6 +299,43 @@ void UIImage::onDrawableResourceEvent( DrawableResource::Event event, DrawableRe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool UIImage::loadFileDrawable( const Network::URI& uri ) {
|
||||||
|
UISceneNode* scene = getUISceneNode();
|
||||||
|
if ( !scene || !scene->getThreadPool() ||
|
||||||
|
!Window::Engine::instance()->isSharedGLContextEnabled() )
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto resourceState = scene->getAsyncResourceLoadState();
|
||||||
|
Uint64 resourceGeneration =
|
||||||
|
resourceState ? resourceState->generation.load( std::memory_order_acquire ) : 0;
|
||||||
|
Uint64 loadId = ++mRemoteImageLoadId;
|
||||||
|
auto alive = mAsyncImageAlive;
|
||||||
|
const std::string filePath = uri.getFSPath();
|
||||||
|
|
||||||
|
scene->getThreadPool()->run(
|
||||||
|
[resourceState, resourceGeneration, alive, loadId, filePath, this] {
|
||||||
|
if ( !UISceneNode::isAsyncResourceLoadCurrent( resourceState, resourceGeneration ) ||
|
||||||
|
!alive || !alive->load( std::memory_order_acquire ) )
|
||||||
|
return;
|
||||||
|
|
||||||
|
Texture* texture = TextureFactory::instance()->loadFromFile(
|
||||||
|
filePath, false, Texture::ClampMode::ClampToEdge, false, false );
|
||||||
|
if ( texture == nullptr )
|
||||||
|
return;
|
||||||
|
|
||||||
|
UISceneNode::runAsyncResourceOnMainThread(
|
||||||
|
resourceState, resourceGeneration, [alive, loadId, texture, this]( UISceneNode* ) {
|
||||||
|
if ( !alive || !alive->load( std::memory_order_acquire ) ||
|
||||||
|
loadId != mRemoteImageLoadId )
|
||||||
|
return;
|
||||||
|
|
||||||
|
setDrawable( texture, false );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void UIImage::loadRemoteDrawable( const Network::URI& uri ) {
|
void UIImage::loadRemoteDrawable( const Network::URI& uri ) {
|
||||||
UISceneNode* scene = getUISceneNode();
|
UISceneNode* scene = getUISceneNode();
|
||||||
if ( !scene )
|
if ( !scene )
|
||||||
@@ -435,9 +478,10 @@ bool UIImage::applyProperty( const StyleSheetProperty& attribute ) {
|
|||||||
std::string path( attribute.getValue() );
|
std::string path( attribute.getValue() );
|
||||||
URI uri( path );
|
URI uri( path );
|
||||||
bool ownIt;
|
bool ownIt;
|
||||||
|
UISceneNode* scene = getUISceneNode();
|
||||||
|
|
||||||
if ( uri.getScheme().empty() && !getUISceneNode()->getURI().empty() ) {
|
if ( scene && uri.getScheme().empty() && !scene->getURI().empty() ) {
|
||||||
uri = getUISceneNode()->solveRelativePath( uri );
|
uri = scene->solveRelativePath( uri );
|
||||||
path = uri.toString();
|
path = uri.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,6 +490,9 @@ bool UIImage::applyProperty( const StyleSheetProperty& attribute ) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( mDeferLoad && uri.getScheme() == "file" && loadFileDrawable( uri ) )
|
||||||
|
break;
|
||||||
|
|
||||||
Drawable* createdDrawable =
|
Drawable* createdDrawable =
|
||||||
StyleSheetSpecification::instance()->getDrawableImageParser().createDrawable(
|
StyleSheetSpecification::instance()->getDrawableImageParser().createDrawable(
|
||||||
path, mSize, ownIt, this );
|
path, mSize, ownIt, this );
|
||||||
@@ -454,7 +501,7 @@ bool UIImage::applyProperty( const StyleSheetProperty& attribute ) {
|
|||||||
} else {
|
} else {
|
||||||
Drawable* res = NULL;
|
Drawable* res = NULL;
|
||||||
if ( NULL != ( res = DrawableSearcher::searchByName(
|
if ( NULL != ( res = DrawableSearcher::searchByName(
|
||||||
path, false, getUISceneNode()->getReferer() ) ) )
|
path, false, scene ? scene->getReferer() : URI() ) ) )
|
||||||
setDrawable( res, res->getDrawableType() == Drawable::SPRITE );
|
setDrawable( res, res->getDrawableType() == Drawable::SPRITE );
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -494,6 +541,9 @@ bool UIImage::applyProperty( const StyleSheetProperty& attribute ) {
|
|||||||
case PropertyId::Height:
|
case PropertyId::Height:
|
||||||
unsetFlags( UI_AUTO_SIZE );
|
unsetFlags( UI_AUTO_SIZE );
|
||||||
return UIWidget::applyProperty( attribute );
|
return UIWidget::applyProperty( attribute );
|
||||||
|
case PropertyId::Defer:
|
||||||
|
mDeferLoad = attribute.getValue().empty() ? true : attribute.asBool();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
return UIWidget::applyProperty( attribute );
|
return UIWidget::applyProperty( attribute );
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ void UILayout::updateLayoutTree() {
|
|||||||
if ( sMetricsEnabled )
|
if ( sMetricsEnabled )
|
||||||
sMetrics.treeUpdates++;
|
sMetrics.treeUpdates++;
|
||||||
|
|
||||||
|
mCurrentLayoutReasons = mDirtyReasons;
|
||||||
mUpdatingLayoutTree = true;
|
mUpdatingLayoutTree = true;
|
||||||
updateLayout();
|
updateLayout();
|
||||||
|
|
||||||
@@ -118,6 +119,7 @@ void UILayout::updateLayoutTree() {
|
|||||||
mUpdatingLayoutTree = false;
|
mUpdatingLayoutTree = false;
|
||||||
mDirtyReasons = 0;
|
mDirtyReasons = 0;
|
||||||
onLayoutUpdate();
|
onLayoutUpdate();
|
||||||
|
mCurrentLayoutReasons = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool UILayout::setMatchParentIfNeededVerticalGrowth() {
|
bool UILayout::setMatchParentIfNeededVerticalGrowth() {
|
||||||
|
|||||||
@@ -2343,37 +2343,19 @@ bool UIRichText::isTextSelectionEnabled() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void UIRichText::onLayoutUpdate() {
|
void UIRichText::onLayoutUpdate() {
|
||||||
// This trashes too much the layout, there must be a better way, also, all tests pass without
|
LayoutInvalidationFlags deferred = mDeferredLayoutReasons;
|
||||||
// this, and also I cannot find an example where this ends up being 100% necessary.
|
mDeferredLayoutReasons = 0;
|
||||||
/* if ( mDeferredLayoutReasons ) {
|
|
||||||
LayoutInvalidationFlags deferred = mDeferredLayoutReasons;
|
|
||||||
mDeferredLayoutReasons = 0;
|
|
||||||
|
|
||||||
LayoutInvalidationFlags nonPaint =
|
LayoutInvalidationFlags nonPaint =
|
||||||
deferred & ~toLayoutInvalidationFlags( LayoutInvalidationReason::PaintOnly );
|
deferred & ~toLayoutInvalidationFlags( LayoutInvalidationReason::PaintOnly );
|
||||||
|
if ( !nonPaint )
|
||||||
|
return;
|
||||||
|
|
||||||
if ( nonPaint ) {
|
LayoutInvalidationFlags newReasons = nonPaint & ~mCurrentLayoutReasons;
|
||||||
const Sizef oldSize = getPixelsSize();
|
if ( !newReasons )
|
||||||
const LayoutInvalidationFlags formattingReasons =
|
return;
|
||||||
toLayoutInvalidationFlags( LayoutInvalidationReason::IntrinsicSize ) |
|
|
||||||
toLayoutInvalidationFlags( LayoutInvalidationReason::FormattingContext ) |
|
setLayoutDirty( nonPaint );
|
||||||
toLayoutInvalidationFlags( LayoutInvalidationReason::Style ) |
|
|
||||||
toLayoutInvalidationFlags( LayoutInvalidationReason::DocumentExtent ) |
|
|
||||||
toLayoutInvalidationFlags( LayoutInvalidationReason::Viewport );
|
|
||||||
if ( nonPaint & formattingReasons )
|
|
||||||
tryUpdateLayout();
|
|
||||||
if ( nonPaint & toLayoutInvalidationFlags( LayoutInvalidationReason::OutOfFlowChild ) )
|
|
||||||
updateOutOfFlowPosition();
|
|
||||||
if ( oldSize != getPixelsSize() ) {
|
|
||||||
LayoutInvalidationFlags parentReasons = LayoutInvalidation::ParentChildChange;
|
|
||||||
if ( nonPaint &
|
|
||||||
toLayoutInvalidationFlags( LayoutInvalidationReason::FormattingContext ) )
|
|
||||||
parentReasons |=
|
|
||||||
toLayoutInvalidationFlags( LayoutInvalidationReason::FormattingContext );
|
|
||||||
notifyLayoutAttrChangeParent( parentReasons );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void UIRichText::setTextSelectionEnabled( bool active ) {
|
void UIRichText::setTextSelectionEnabled( bool active ) {
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ EE_MAIN_FUNC int main( int argc, char** argv ) {
|
|||||||
ui->setColorSchemePreference(
|
ui->setColorSchemePreference(
|
||||||
!prefersColorScheme.Get().empty()
|
!prefersColorScheme.Get().empty()
|
||||||
? ColorSchemePreferences::fromStringExt( prefersColorScheme.Get() )
|
? ColorSchemePreferences::fromStringExt( prefersColorScheme.Get() )
|
||||||
: ColorSchemeExtPreference::Light );
|
: ColorSchemeExtPreference::System );
|
||||||
|
|
||||||
bool useHNDark = hnDark.Get();
|
bool useHNDark = hnDark.Get();
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
#include <eepp/window/engine.hpp>
|
#include <eepp/window/engine.hpp>
|
||||||
#include <eepp/window/input.hpp>
|
#include <eepp/window/input.hpp>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
@@ -4065,8 +4066,10 @@ UTEST( UIHTML, TextureReplaceInvalidatesRichTextAncestors ) {
|
|||||||
SceneManager::instance()->update();
|
SceneManager::instance()->update();
|
||||||
documentScene->updateDirtyLayouts();
|
documentScene->updateDirtyLayouts();
|
||||||
|
|
||||||
auto* article = documentScene->getRoot()->find( "article" )->asType<UIRichText>();
|
auto* articleNode = documentScene->getRoot()->find( "article" );
|
||||||
auto* body = documentScene->getRoot()->findByType( UI_TYPE_HTML_BODY )->asType<UIWidget>();
|
auto* bodyNode = documentScene->getRoot()->findByType( UI_TYPE_HTML_BODY );
|
||||||
|
auto* article = articleNode ? articleNode->asType<UIRichText>() : nullptr;
|
||||||
|
auto* body = bodyNode ? bodyNode->asType<UIWidget>() : nullptr;
|
||||||
auto* doc = webView->getDocumentContainer();
|
auto* doc = webView->getDocumentContainer();
|
||||||
auto images = documentScene->getRoot()->findAllByTag( "img" );
|
auto images = documentScene->getRoot()->findAllByTag( "img" );
|
||||||
ASSERT_TRUE( article != nullptr );
|
ASSERT_TRUE( article != nullptr );
|
||||||
@@ -4098,6 +4101,348 @@ UTEST( UIHTML, TextureReplaceInvalidatesRichTextAncestors ) {
|
|||||||
Engine::destroySingleton();
|
Engine::destroySingleton();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UTEST( UIHTML, DeferredFileImageLoadsAsync ) {
|
||||||
|
auto win = Engine::instance()->createWindow(
|
||||||
|
WindowSettings( 1024, 768, "deferred file img async", WindowStyle::Default,
|
||||||
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
||||||
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
||||||
|
const std::string processPath = Sys::getProcessPath();
|
||||||
|
const std::string imagePath = processPath + "../assets/icon/ee.png";
|
||||||
|
ASSERT_TRUE( FileSystem::fileExists( imagePath ) );
|
||||||
|
|
||||||
|
auto threadPool = ThreadPool::createShared( 1 );
|
||||||
|
std::atomic<bool> releaseBlocker{ false };
|
||||||
|
std::atomic<bool> blockerStarted{ false };
|
||||||
|
threadPool->run( [&] {
|
||||||
|
blockerStarted.store( true, std::memory_order_release );
|
||||||
|
while ( !releaseBlocker.load( std::memory_order_acquire ) )
|
||||||
|
Sys::sleep( Milliseconds( 1 ) );
|
||||||
|
} );
|
||||||
|
|
||||||
|
for ( int i = 0; i < 100 && !blockerStarted.load( std::memory_order_acquire ); ++i )
|
||||||
|
Sys::sleep( Milliseconds( 1 ) );
|
||||||
|
if ( !blockerStarted.load( std::memory_order_acquire ) )
|
||||||
|
releaseBlocker.store( true, std::memory_order_release );
|
||||||
|
ASSERT_TRUE( blockerStarted.load( std::memory_order_acquire ) );
|
||||||
|
|
||||||
|
UISceneNode* sceneNode = init_test_inline_block();
|
||||||
|
sceneNode->setThreadPool( threadPool );
|
||||||
|
sceneNode->setURI( URI( "file://" + processPath ) );
|
||||||
|
|
||||||
|
std::string html = R"html(
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<img id="deferred-img" defer src="../assets/icon/ee.png">
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)html";
|
||||||
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ), sceneNode->getRoot(),
|
||||||
|
String::hash( "deferred-file-image" ) );
|
||||||
|
|
||||||
|
win->getInput()->update();
|
||||||
|
SceneManager::instance()->update();
|
||||||
|
sceneNode->updateDirtyLayouts();
|
||||||
|
|
||||||
|
auto* imgNode = sceneNode->getRoot()->find( "deferred-img" );
|
||||||
|
if ( imgNode == nullptr )
|
||||||
|
releaseBlocker.store( true, std::memory_order_release );
|
||||||
|
ASSERT_TRUE( imgNode != nullptr );
|
||||||
|
auto* img = imgNode->asType<UIHTMLImage>();
|
||||||
|
if ( img == nullptr )
|
||||||
|
releaseBlocker.store( true, std::memory_order_release );
|
||||||
|
ASSERT_TRUE( img != nullptr );
|
||||||
|
EXPECT_TRUE( img->getDrawable() == nullptr );
|
||||||
|
|
||||||
|
releaseBlocker.store( true, std::memory_order_release );
|
||||||
|
|
||||||
|
for ( int i = 0; i < 200 && img->getDrawable() == nullptr; ++i ) {
|
||||||
|
win->getInput()->update();
|
||||||
|
SceneManager::instance()->update();
|
||||||
|
sceneNode->updateDirtyLayouts();
|
||||||
|
Sys::sleep( Milliseconds( 1 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
ASSERT_TRUE( img->getDrawable() != nullptr );
|
||||||
|
EXPECT_GT( img->getPixelsSize().getWidth(), 0.f );
|
||||||
|
EXPECT_GT( img->getPixelsSize().getHeight(), 0.f );
|
||||||
|
|
||||||
|
Engine::destroySingleton();
|
||||||
|
}
|
||||||
|
|
||||||
|
UTEST( UIHTML, DeferredFileImageRelayoutsAncestors ) {
|
||||||
|
auto win = Engine::instance()->createWindow(
|
||||||
|
WindowSettings( 1024, 768, "deferred file img relayout", WindowStyle::Default,
|
||||||
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
||||||
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
||||||
|
|
||||||
|
const std::string imageName = "eepp_deferred_file_image_relayout.png";
|
||||||
|
const std::string imagePath = Sys::getTempPath() + imageName;
|
||||||
|
Image testImage( 320, 180, 4, Color::White );
|
||||||
|
ASSERT_TRUE( testImage.saveToFile( imagePath, Image::SaveType::PNG ) );
|
||||||
|
|
||||||
|
UISceneNode* sceneNode = init_test_inline_block();
|
||||||
|
UIWebView* webView = UIWebView::New();
|
||||||
|
webView->setParent( sceneNode->getRoot() );
|
||||||
|
webView->setPixelsSize( win->getWidth(), win->getHeight() );
|
||||||
|
webView->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
|
||||||
|
|
||||||
|
UISceneNode* documentScene = webView->getDocumentSceneNode();
|
||||||
|
ASSERT_TRUE( documentScene != nullptr );
|
||||||
|
|
||||||
|
auto threadPool = ThreadPool::createShared( 1 );
|
||||||
|
std::atomic<bool> releaseBlocker{ false };
|
||||||
|
std::atomic<bool> blockerStarted{ false };
|
||||||
|
threadPool->run( [&] {
|
||||||
|
blockerStarted.store( true, std::memory_order_release );
|
||||||
|
while ( !releaseBlocker.load( std::memory_order_acquire ) )
|
||||||
|
Sys::sleep( Milliseconds( 1 ) );
|
||||||
|
} );
|
||||||
|
|
||||||
|
for ( int i = 0; i < 100 && !blockerStarted.load( std::memory_order_acquire ); ++i )
|
||||||
|
Sys::sleep( Milliseconds( 1 ) );
|
||||||
|
if ( !blockerStarted.load( std::memory_order_acquire ) )
|
||||||
|
releaseBlocker.store( true, std::memory_order_release );
|
||||||
|
ASSERT_TRUE( blockerStarted.load( std::memory_order_acquire ) );
|
||||||
|
|
||||||
|
documentScene->setThreadPool( threadPool );
|
||||||
|
documentScene->setURI( URI( "file://" + Sys::getTempPath() ) );
|
||||||
|
|
||||||
|
std::string html = R"html(
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<article id="article" style="display:block; width: 480px;">
|
||||||
|
<p>Before image</p>
|
||||||
|
<img id="async-img" defer src=")html" +
|
||||||
|
imageName +
|
||||||
|
R"html(">
|
||||||
|
<p id="after-img">After image</p>
|
||||||
|
</article>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)html";
|
||||||
|
documentScene->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ),
|
||||||
|
webView->getDocumentContainer(),
|
||||||
|
String::hash( "deferred-file-image-relayout" ) );
|
||||||
|
|
||||||
|
win->getInput()->update();
|
||||||
|
SceneManager::instance()->update();
|
||||||
|
documentScene->updateDirtyLayouts();
|
||||||
|
|
||||||
|
auto* article = documentScene->getRoot()->find( "article" )->asType<UIRichText>();
|
||||||
|
auto* body = documentScene->getRoot()->findByType( UI_TYPE_HTML_BODY )->asType<UIWidget>();
|
||||||
|
auto* doc = webView->getDocumentContainer();
|
||||||
|
auto* imgNode = documentScene->getRoot()->find( "async-img" );
|
||||||
|
if ( article == nullptr || body == nullptr || doc == nullptr || imgNode == nullptr )
|
||||||
|
releaseBlocker.store( true, std::memory_order_release );
|
||||||
|
ASSERT_TRUE( article != nullptr );
|
||||||
|
ASSERT_TRUE( body != nullptr );
|
||||||
|
ASSERT_TRUE( doc != nullptr );
|
||||||
|
ASSERT_TRUE( imgNode != nullptr );
|
||||||
|
auto* img = imgNode->asType<UIHTMLImage>();
|
||||||
|
if ( img == nullptr )
|
||||||
|
releaseBlocker.store( true, std::memory_order_release );
|
||||||
|
ASSERT_TRUE( img != nullptr );
|
||||||
|
EXPECT_TRUE( img->getDrawable() == nullptr );
|
||||||
|
|
||||||
|
Float articleInitialHeight = article->getPixelsSize().getHeight();
|
||||||
|
Float bodyInitialHeight = body->getPixelsSize().getHeight();
|
||||||
|
Float docInitialHeight = doc->getPixelsSize().getHeight();
|
||||||
|
|
||||||
|
releaseBlocker.store( true, std::memory_order_release );
|
||||||
|
|
||||||
|
for ( int i = 0; i < 200 && article->getPixelsSize().getHeight() < articleInitialHeight + 150.f;
|
||||||
|
++i ) {
|
||||||
|
win->getInput()->update();
|
||||||
|
SceneManager::instance()->update();
|
||||||
|
documentScene->updateDirtyLayouts();
|
||||||
|
Sys::sleep( Milliseconds( 1 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
ASSERT_TRUE( img->getDrawable() != nullptr );
|
||||||
|
EXPECT_GT( article->getPixelsSize().getHeight(), articleInitialHeight + 150.f );
|
||||||
|
EXPECT_GT( body->getPixelsSize().getHeight(), bodyInitialHeight + 150.f );
|
||||||
|
EXPECT_GT( doc->getPixelsSize().getHeight(), docInitialHeight + 150.f );
|
||||||
|
|
||||||
|
FileSystem::fileRemove( imagePath );
|
||||||
|
Engine::destroySingleton();
|
||||||
|
}
|
||||||
|
|
||||||
|
UTEST( UIHTML, RonStonerDeferredImagesUpdateDocumentHeight ) {
|
||||||
|
auto threadPool = ThreadPool::createShared( eemax<int>( 4, Sys::getCPUCount() ) );
|
||||||
|
auto win = Engine::instance()->createWindow(
|
||||||
|
WindowSettings( 1280, 720, "ron stoner deferred image relayout", WindowStyle::Default,
|
||||||
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
||||||
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
||||||
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
||||||
|
|
||||||
|
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" );
|
||||||
|
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
||||||
|
ASSERT_TRUE( font != nullptr && font->loaded() );
|
||||||
|
FontFamily::loadFromRegular( font );
|
||||||
|
|
||||||
|
UISceneNode* sceneNode = UISceneNode::New();
|
||||||
|
SceneManager::instance()->add( sceneNode );
|
||||||
|
SceneManager::instance()->setCurrentUISceneNode( sceneNode );
|
||||||
|
sceneNode->setThreadPool( threadPool );
|
||||||
|
sceneNode->getUIThemeManager()->setDefaultFont( font );
|
||||||
|
|
||||||
|
UIWebView* webView = UIWebView::New();
|
||||||
|
webView->setParent( sceneNode->getRoot() );
|
||||||
|
webView->setPixelsSize( win->getWidth(), win->getHeight() );
|
||||||
|
webView->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
|
||||||
|
|
||||||
|
const std::string path = Sys::getProcessPath() + "assets/html/ron_stoner.html";
|
||||||
|
ASSERT_TRUE( FileSystem::fileExists( path ) );
|
||||||
|
webView->loadURI( URI( "file://" + path ) );
|
||||||
|
|
||||||
|
auto pump = [&] {
|
||||||
|
win->getInput()->update();
|
||||||
|
SceneManager::instance()->update( Seconds( 1.f / 60.f ) );
|
||||||
|
if ( auto* documentScene = webView->getDocumentSceneNode() )
|
||||||
|
documentScene->updateDirtyLayouts();
|
||||||
|
Sys::sleep( Milliseconds( 1 ) );
|
||||||
|
};
|
||||||
|
|
||||||
|
UISceneNode* documentScene = webView->getDocumentSceneNode();
|
||||||
|
ASSERT_TRUE( documentScene != nullptr );
|
||||||
|
|
||||||
|
UIWidget* body = nullptr;
|
||||||
|
std::vector<UIWidget*> images;
|
||||||
|
for ( int i = 0; i < 300; ++i ) {
|
||||||
|
pump();
|
||||||
|
body = documentScene->getRoot()->findByType( UI_TYPE_HTML_BODY )->asType<UIWidget>();
|
||||||
|
images = documentScene->getRoot()->findAllByTag( "img" );
|
||||||
|
if ( body != nullptr && !images.empty() )
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ASSERT_TRUE( body != nullptr );
|
||||||
|
ASSERT_FALSE( images.empty() );
|
||||||
|
|
||||||
|
auto allImagesLoaded = [&images] {
|
||||||
|
for ( auto* image : images ) {
|
||||||
|
auto* uiImage = image ? image->asType<UIImage>() : nullptr;
|
||||||
|
if ( uiImage == nullptr || uiImage->getDrawable() == nullptr )
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
for ( int i = 0; i < 600 && !allImagesLoaded(); ++i )
|
||||||
|
pump();
|
||||||
|
|
||||||
|
ASSERT_TRUE( allImagesLoaded() );
|
||||||
|
|
||||||
|
for ( int i = 0; i < 30; ++i )
|
||||||
|
pump();
|
||||||
|
|
||||||
|
const Float bodyHeightAfterAsyncLoad = body->getPixelsSize().getHeight();
|
||||||
|
const Float docHeightAfterAsyncLoad =
|
||||||
|
webView->getDocumentContainer()->getPixelsSize().getHeight();
|
||||||
|
ASSERT_GT( bodyHeightAfterAsyncLoad, 1000.f );
|
||||||
|
ASSERT_GT( docHeightAfterAsyncLoad, 1000.f );
|
||||||
|
|
||||||
|
webView->setPixelsSize( 1180, 680 );
|
||||||
|
for ( int i = 0; i < 30; ++i )
|
||||||
|
pump();
|
||||||
|
webView->setPixelsSize( win->getWidth(), win->getHeight() );
|
||||||
|
for ( int i = 0; i < 30; ++i )
|
||||||
|
pump();
|
||||||
|
|
||||||
|
const Float bodyHeightAfterResize = body->getPixelsSize().getHeight();
|
||||||
|
const Float docHeightAfterResize = webView->getDocumentContainer()->getPixelsSize().getHeight();
|
||||||
|
|
||||||
|
EXPECT_GE( bodyHeightAfterAsyncLoad + 1.f, bodyHeightAfterResize );
|
||||||
|
EXPECT_GE( docHeightAfterAsyncLoad + 1.f, docHeightAfterResize );
|
||||||
|
|
||||||
|
Engine::destroySingleton();
|
||||||
|
}
|
||||||
|
|
||||||
|
UTEST( UIHTML, NewsBlurStoryArchiveLayoutStabilizes ) {
|
||||||
|
auto win = Engine::instance()->createWindow(
|
||||||
|
WindowSettings( 1280, 720, "newsblur story archive layout stability", WindowStyle::Default,
|
||||||
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
||||||
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
||||||
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
||||||
|
|
||||||
|
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" );
|
||||||
|
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
||||||
|
ASSERT_TRUE( font != nullptr && font->loaded() );
|
||||||
|
FontFamily::loadFromRegular( font );
|
||||||
|
|
||||||
|
UISceneNode* sceneNode = UISceneNode::New();
|
||||||
|
SceneManager::instance()->add( sceneNode );
|
||||||
|
SceneManager::instance()->setCurrentUISceneNode( sceneNode );
|
||||||
|
sceneNode->getUIThemeManager()->setDefaultFont( font );
|
||||||
|
|
||||||
|
UIWebView* webView = UIWebView::New();
|
||||||
|
webView->setParent( sceneNode->getRoot() );
|
||||||
|
webView->setPixelsSize( win->getWidth(), win->getHeight() );
|
||||||
|
webView->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
|
||||||
|
|
||||||
|
const std::string path = Sys::getProcessPath() + "assets/html/story_archive_newsblur.html";
|
||||||
|
ASSERT_TRUE( FileSystem::fileExists( path ) );
|
||||||
|
webView->loadURI( URI( "file://" + path ) );
|
||||||
|
|
||||||
|
auto pump = [&] {
|
||||||
|
win->getInput()->update();
|
||||||
|
SceneManager::instance()->update( Seconds( 1.f / 60.f ) );
|
||||||
|
if ( auto* documentScene = webView->getDocumentSceneNode() )
|
||||||
|
documentScene->updateDirtyLayouts();
|
||||||
|
Sys::sleep( Milliseconds( 1 ) );
|
||||||
|
};
|
||||||
|
|
||||||
|
UISceneNode* documentScene = webView->getDocumentSceneNode();
|
||||||
|
ASSERT_TRUE( documentScene != nullptr );
|
||||||
|
|
||||||
|
UIWidget* body = nullptr;
|
||||||
|
for ( int i = 0; i < 300; ++i ) {
|
||||||
|
pump();
|
||||||
|
auto* bodyNode = documentScene->getRoot()->findByType( UI_TYPE_HTML_BODY );
|
||||||
|
body = bodyNode ? bodyNode->asType<UIWidget>() : nullptr;
|
||||||
|
if ( body != nullptr && body->getPixelsSize().getHeight() > 0 )
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ASSERT_TRUE( body != nullptr );
|
||||||
|
|
||||||
|
bool stabilized = false;
|
||||||
|
Uint64 lastInvalidations = 0;
|
||||||
|
Uint64 lastTreeUpdates = 0;
|
||||||
|
Uint64 lastRichTextRebuilds = 0;
|
||||||
|
int stableFrames = 0;
|
||||||
|
for ( int i = 0; i < 240; ++i ) {
|
||||||
|
UILayout::resetMetrics();
|
||||||
|
pump();
|
||||||
|
auto metrics = UILayout::getMetrics();
|
||||||
|
lastInvalidations = metrics.invalidations;
|
||||||
|
lastTreeUpdates = metrics.treeUpdates;
|
||||||
|
lastRichTextRebuilds = metrics.richTextRebuilds;
|
||||||
|
|
||||||
|
if ( metrics.invalidations == 0 && metrics.treeUpdates == 0 &&
|
||||||
|
metrics.richTextRebuilds == 0 ) {
|
||||||
|
if ( ++stableFrames >= 10 ) {
|
||||||
|
stabilized = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stableFrames = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UILayout::setMetricsEnabled( false );
|
||||||
|
|
||||||
|
if ( !stabilized ) {
|
||||||
|
std::cout << "NewsBlur layout did not stabilize: lastInvalidations=" << lastInvalidations
|
||||||
|
<< " lastTreeUpdates=" << lastTreeUpdates
|
||||||
|
<< " lastRichTextRebuilds=" << lastRichTextRebuilds << std::endl;
|
||||||
|
}
|
||||||
|
EXPECT_TRUE( stabilized );
|
||||||
|
|
||||||
|
Engine::destroySingleton();
|
||||||
|
}
|
||||||
|
|
||||||
UTEST( UIHTML, HtmlContainsTableBodyHeight ) {
|
UTEST( UIHTML, HtmlContainsTableBodyHeight ) {
|
||||||
auto win = Engine::instance()->createWindow(
|
auto win = Engine::instance()->createWindow(
|
||||||
WindowSettings( 1024, 768, "html contains table body height", WindowStyle::Default,
|
WindowSettings( 1024, 768, "html contains table body height", WindowStyle::Default,
|
||||||
|
|||||||