diff --git a/assets/Defer.js b/assets/Defer.js
new file mode 100644
index 0000000..71c9d8c
--- /dev/null
+++ b/assets/Defer.js
@@ -0,0 +1,87 @@
+export default function (count = 20) {
+ return {
+ data() {
+ return {
+ displayPriority: 0,
+ };
+ },
+
+ mounted() {
+ this.runDisplayPriority();
+ },
+
+ computed: {
+ numShuffles() {
+ return this.$store.state.numShuffles;
+ },
+
+ layout() {
+ return this.$store.state.layout;
+ },
+
+ showNsfw() {
+ return this.$store.state.showNsfw;
+ },
+
+ filterNsfw() {
+ return this.$store.state.filterNsfw;
+ },
+ },
+
+ watch: {
+ // Watch the store values that cause the most intense operations
+ // when they change so mounting posts can be deferred. The display
+ // priority val has to be reset each time deferring is needed.
+ numShuffles: {
+ deep: false,
+ handler() {
+ this.resetDisplayPriority();
+ },
+ },
+
+ layout: {
+ deep: false,
+ handler() {
+ this.resetDisplayPriority();
+ },
+ },
+
+ showNsfw: {
+ deep: false,
+ handler() {
+ this.resetDisplayPriority();
+ },
+ },
+
+ filterNsfw: {
+ deep: false,
+ handler() {
+ this.resetDisplayPriority();
+ },
+ },
+ },
+
+ methods: {
+ runDisplayPriority() {
+ const step = () => {
+ requestAnimationFrame(() => {
+ this.displayPriority++;
+ if (this.displayPriority < count) {
+ step();
+ }
+ });
+ }
+ step();
+ },
+
+ defer(priority) {
+ return this.displayPriority >= priority;
+ },
+
+ resetDisplayPriority() {
+ this.displayPriority = 0;
+ this.runDisplayPriority();
+ }
+ },
+ };
+}
diff --git a/assets/Shuffle.js b/assets/Shuffle.js
new file mode 100644
index 0000000..430d4c5
--- /dev/null
+++ b/assets/Shuffle.js
@@ -0,0 +1,35 @@
+// Randomize array elements without mutating the original array using the
+// Durstenfeld shuffle algorithm.
+export function shuffleArray(array) {
+ let shuffledArray = [...array];
+ for (let i = shuffledArray.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]];
+ }
+ return shuffledArray;
+}
+
+// Randomize array elements without mutating the original array using a
+// modification of Mike Bostock's implementation of the Fisher-Yates algorithm.
+export function shuffleArrayWithSeed(array, seed) {
+ let shuffledArray = [...array];
+ let toShuffleCount = shuffledArray.length;
+
+ while (toShuffleCount) {
+ let randIndex = Math.floor(random(seed) * toShuffleCount--);
+
+ let swapTmp = shuffledArray[toShuffleCount];
+ shuffledArray[toShuffleCount] = shuffledArray[randIndex];
+ shuffledArray[randIndex] = swapTmp;
+
+ ++seed;
+ }
+
+ return shuffledArray;
+}
+
+function random(seed) {
+ let x = Math.sin(seed++) * 10000;
+ let r = x - Math.floor(x);
+ return r;
+}
diff --git a/components/Grid.vue b/components/Grid.vue
index 7c8630d..fa4e67a 100644
--- a/components/Grid.vue
+++ b/components/Grid.vue
@@ -13,13 +13,23 @@
:gutter="{ default: '30px' }"
>
+
+
+
@@ -97,6 +107,7 @@
diff --git a/components/Toolbar.vue b/components/Toolbar.vue
index 5eda9b1..d1a98de 100644
--- a/components/Toolbar.vue
+++ b/components/Toolbar.vue
@@ -18,6 +18,42 @@
+
+
+
+
+
+ Reload posts
+
@@ -64,12 +106,6 @@
z-index: 1;
}
- button {
- color: var(--color-button-dark-text);
- background: transparent;
- border: none;
- }
-
.toolbar__controls {
display: flex;
@@ -84,6 +120,8 @@
justify-content: center;
align-items: center;
+ @include font-main();
+
@include respond-above(sm) {
padding-left: 0;
@@ -114,7 +152,6 @@
height: 40px;
padding: 0 20px;
- @include font-main();
font-size: 18px;
line-height: 40px;
@@ -147,20 +184,52 @@
color: var(--color-button-text);
span {
- @include font-main();
font-size: 16px;
line-height: 1;
}
}
- .toolbar__reload, .toolbar__menu {
+ .toolbar__tooltip {
+ position: absolute;
+ opacity: 0;
+ z-index: 2;
+
+ bottom: -25px;
+ left: 0px;
+
+ padding: 6px;
+
+ font-size: small;
+ white-space: nowrap;
+
+ border-radius: 5px;
+ color: var(--color-text-copy);
+ background-color: var(--color-background-faded);
+
+ transition: opacity .1s;
+ }
+
+ button {
+ position: relative;
+
+ color: var(--color-button-dark-text);
+ background: transparent;
+ border: none;
+
+ &:hover .toolbar__tooltip {
+ opacity: 1;
+ transition: opacity .1s linear 1s;
+ }
+ }
+ .toolbar__reload, .toolbar__menu, .toolbar__extra-icon {
flex: 0 0 auto;
- margin-left: 10px;
+ margin-left: 5px;
+ padding: 0;
- width: 40px;
- height: 40px;
+ width: 35px;
+ height: 35px;
border: none;
background: none;
@@ -171,8 +240,6 @@
.toolbar__menu {
position: relative;
- margin-left: 0;
-
&:after {
content: '';
@@ -200,8 +267,6 @@
}
.toolbar__reload {
- margin-left: 12px;
-
&.toolbar__reload--loading {
i {
opacity: 0.4;
@@ -213,6 +278,21 @@
}
}
}
+
+ @keyframes grow {
+ 0% {
+ scale: 0;
+ margin-left: 0;
+ }
+ 100% {
+ scale: 1;
+ margin-left: 12px;
+ }
+ }
+
+ .toolbar__extra-icon {
+ animation: grow .1s;
+ }
diff --git a/store/index.js b/store/index.js
index 755b5b5..84d6884 100644
--- a/store/index.js
+++ b/store/index.js
@@ -4,6 +4,7 @@ import decode from 'unescape';
import sanitizeHtml from 'sanitize-html';
import aes from 'crypto-js/aes';
import encUtf8 from 'crypto-js/enc-utf8';
+import { shuffleArray, shuffleArrayWithSeed } from '~/assets/Shuffle';
// When enabled, use naive filtering/searching algorithm instead of Fuse.js.
const useNaiveFilter = false;
@@ -78,6 +79,31 @@ export const state = () => ({
* Toast.
*/
toast: null,
+
+ /**
+ * The number of times the user has shuffled posts.
+ */
+ numShuffles: 0,
+
+ /**
+ * The number of shuffled posts to show at a time. 30 is the default because
+ * it is a good middle ground between only showing a few shuffled posts and
+ * showing all shuffled posts while still being clear to the user that not
+ * all posts have been shuffled. Shuffling all posts is not the default
+ * because it can hurt performance to do it repeatedly with a lot of posts
+ * (more than several hundred because of the time to unmount then remount
+ * that many elements at once). If users want to shuffle all posts at
+ * once, they will have to navigate to the menu and see the message about it
+ * impacting performance first - not to dicourage them from shuffling all
+ * posts, but to make sure they are not shuffling all posts if what they
+ * really want is just a few shuffled at a time.
+ */
+ numShuffledPosts: 30,
+
+ /**
+ * The seed to use when shuffling posts with a seed.
+ */
+ shuffleSeed: Math.floor(Math.random() * 100),
});
/**
@@ -90,6 +116,8 @@ function saveSettings(state) {
filterNsfw: state.filterNsfw,
subreddits: state.subreddits,
layout: state.layout,
+ numShuffledPosts: state.numShuffledPosts,
+ shuffleSeed: state.shuffleSeed,
}));
}
@@ -104,14 +132,19 @@ function restoreSettings(state) {
savedSettings = JSON.parse(savedSettings);
} catch (e) {
savedSettings = null;
+ }
+
+ if (savedSettings) {
+ state.theme = savedSettings.theme != undefined ? savedSettings.theme : state.theme;
+ state.showNsfw = savedSettings.showNsfw != undefined ? savedSettings.showNsfw : state.showNsfw;
+ state.filterNsfw = savedSettings.filterNsfw != undefined ? savedSettings.filterNsfw : state.filterNsfw;
+ state.subreddits = savedSettings.subreddits != undefined ? savedSettings.subreddits : state.subreddits;
+ state.numShuffledPosts = savedSettings.numShuffledPosts != undefined ? savedSettings.numShuffledPosts : state.numShuffledPosts;
+ // only restore the shuffle seed if it was set to null by the user
+ state.shuffleSeed = savedSettings.shuffleSeed === null ? null : state.shuffleSeed;
+
+ state.layout = savedSettings.layout ? savedSettings.layout : 'fixed';
}
-
- state.theme = savedSettings && savedSettings.theme != null ? savedSettings.theme : state.theme;
- state.showNsfw = savedSettings && savedSettings.showNsfw != null ? savedSettings.showNsfw : state.showNsfw;
- state.filterNsfw = savedSettings && savedSettings.showNsfw != null ? savedSettings.filterNsfw : state.filterNsfw;
- state.subreddits = savedSettings && savedSettings.showNsfw != null ? savedSettings.subreddits : state.subreddits;
-
- state.layout = savedSettings && savedSettings.layout ? savedSettings.layout : 'fixed';
}
/**
@@ -276,6 +309,32 @@ export const getters = {
posts = posts.filter(p => validSubreddits.includes(p.subreddit));
}
+ // Apply randomized shuffle. Do this last so users can shuffle a subset
+ // of posts based on search, filters, or subreddits.
+ if (state.numShuffles && posts.length) {
+ const shuffleAllPosts =
+ state.numShuffledPosts <= 0 ||
+ state.numShuffledPosts >= posts.length;
+
+ const useTrueRandomization = state.shuffleSeed === null;
+
+ if (shuffleAllPosts) {
+ posts = shuffleArray(posts);
+ } else if (useTrueRandomization) {
+ posts = shuffleArray(posts).slice(0, state.numShuffledPosts);
+ } else {
+ // Guaruntees no post is shown twice before all other posts have
+ // been shown.
+ posts = shuffleArrayWithSeed(posts, state.shuffleSeed);
+
+ let start = state.numShuffles * state.numShuffledPosts % posts.length;
+ let end = start + state.numShuffledPosts;
+ let overflow = Math.max(end - posts.length, 0);
+
+ posts = [...posts.slice(start, end), ...posts.slice(0, overflow)];
+ }
+ }
+
return posts;
},
};
@@ -406,6 +465,26 @@ export const mutations = {
message,
};
},
+
+ SET_SHUFFLE(state) {
+ state.numShuffles++;
+ },
+
+ RESET_SHUFFLE(state) {
+ state.numShuffles = 0;
+ },
+
+ SET_NUM_SHUFFLED_POSTS(state, num) {
+ state.numShuffledPosts = num;
+
+ saveSettings(state);
+ },
+
+ SET_SHUFFLE_SEED(state, seed) {
+ state.shuffleSeed = seed;
+
+ saveSettings(state);
+ },
};
export const actions = {
@@ -694,4 +773,41 @@ export const actions = {
setTheme({ commit }, theme) {
commit('SET_THEME', theme);
},
+
+ /**
+ * Sets true randomization true or false, changing the shuffle seed when
+ * setting to false so users get a new seeded shuffle every time they
+ * toggle the option off and back on.
+ */
+ setTrueRandomization({ commit }, trueRandomization) {
+ if (trueRandomization) {
+ commit('SET_SHUFFLE_SEED', null);
+ } else {
+ commit('SET_SHUFFLE_SEED', Math.floor(Math.random() * 100));
+ }
+ },
+
+ /**
+ * Shuffles posts.
+ */
+ shuffle({ commit }) {
+ commit('SET_SHUFFLE');
+ },
+
+ /**
+ * Resets shuffled posts so they are displayed in original order again.
+ */
+ resetShuffle({ commit, state }) {
+ commit('RESET_SHUFFLE');
+ if (state.shuffleSeed) {
+ commit('SET_SHUFFLE_SEED', Math.floor(Math.random() * 100));
+ }
+ },
+
+ /**
+ * Sets the number of shuffled posts to display.
+ */
+ setNumShuffledPosts({ commit }, num) {
+ commit('SET_NUM_SHUFFLED_POSTS', num);
+ },
};