+
+ v{{ commit.version }}
+
+
({{ formatDate(commit.date) }})
+
+ {{ entry.key }}
+
+
@@ -111,7 +149,21 @@
import {get} from 'utilities/object';
-const TITLE_MATCH = /^v?(\d+\.\d+\.\d+(?:-[^\n]+)?)\n+/;
+const TITLE_MATCH = /^(.+?)?\s*v?(\d+\.\d+\.\d+(?:\-[a-z0-9-]+)?)$/i,
+ SETTING_REGEX = /\]\(~([^)]+)\)/g,
+ CHANGE_REGEX = /^\*\s*([^:]+?):\s*(.+)$/i,
+ ISSUE_REGEX = /(^|\s)#(\d+)\b/g;
+
+
+function linkify(text, repo) {
+ text = text.replace(SETTING_REGEX, (_, link) => {
+ return `](~${link})`
+ });
+
+ return text.replace(ISSUE_REGEX, (_, space, number) => {
+ return `${space}[#${number}](https://github.com/FrankerFaceZ/${repo}/issues/${number})`;
+ });
+}
export default {
@@ -120,6 +172,7 @@ export default {
data() {
return {
error: false,
+ addon: this.item.addon,
addons: this.item.addons,
nonversioned: false,
loading: false,
@@ -130,35 +183,130 @@ export default {
computed: {
display() {
+ window.thing = this;
+
const out = [],
+ addons = this.addons ? this.item.getFFZ().resolve('addons') : null,
old_commit = this.t('home.changelog.nonversioned', 'Non-Versioned Commit');
for(const commit of this.commits) {
- let message = commit.commit.message,
+ const input = commit.commit.message;
+ let title = old_commit,
+ title_nav = null,
+ icon = null,
+ version = null,
author = null,
- title = old_commit;
+ sections = {},
+ description = [];
- if ( this.addons ) {
- title = null;
- author = commit.author;
+ if ( /\bskiplog\b/i.test(input) && ! this.nonversion )
+ continue;
- } else {
- const match = TITLE_MATCH.exec(message);
-
- if ( match ) {
- title = match[1];
- message = message.slice(match[0].length);
- } else if ( ! this.nonversioned )
- continue;
- }
+ const lines = input.split(/\r?\n/),
+ first = lines.shift(),
+ match = first ? TITLE_MATCH.exec(first) : null;
const date = new Date(commit.commit.author.date),
- active = commit.sha === window.FrankerFaceZ.version_info.commit;
+ active = commit.sha === window.FrankerFaceZ.version_info.commit,
+ has_content = lines.length && match;
+
+ if ( ! this.nonversion && ! has_content )
+ continue;
+
+ let last_bit = null;
+
+ if ( match ) {
+ title = match[1];
+ version = match[2];
+ }
+
+ if ( has_content )
+ for(const line of lines) {
+ const trimmed = line.trim();
+ if ( ! trimmed.length ) {
+ if ( ! last_bit && description.length )
+ description.push(line);
+ continue;
+ }
+
+ const m = CHANGE_REGEX.exec(trimmed);
+ if ( ! m ) {
+ if ( ! last_bit )
+ description.push(line);
+ else
+ last_bit.push(trimmed);
+
+ } else {
+ const section = sections[m[1]] = sections[m[1]] || [];
+ last_bit = [m[2]];
+ section.push(last_bit);
+ }
+ }
+
+ else {
+ lines.unshift(first);
+ description = lines;
+ }
+
+ let message = description.join('\n').trim();
+
+ const segments = [];
+
+ for(const [key, val] of Object.entries(sections)) {
+ if ( ! val?.length )
+ continue;
+
+ const bit = val.map(x => `* ${x.join(' ')}`).join('\n').trim();
+
+ segments.push({
+ key,
+ value: linkify(bit, this.addons ? 'add-ons' : 'frankerfacez')
+ });
+ }
+
+ if ( this.addons ) {
+ author = commit.author;
+
+ if ( title ) {
+ const ltitle = title.toLowerCase();
+
+ if ( addons?.addons )
+ for(const addon of Object.values(addons.addons)) {
+ if ((addon.short_name && addon.short_name.toLowerCase() === ltitle) ||
+ (addon.name && addon.name.toLowerCase() === ltitle) ||
+ (addon.id && addon.id.toLowerCase() === ltitle)
+ ) {
+ icon = addon.icon;
+
+ title_nav = [`add_ons.changelog.${addon.id}`];
+ if ( addon.short_name )
+ title_nav.push(`add_ons.changelog.${addon.short_name.toSnakeCase()}`);
+ if ( addon.name )
+ title_nav.push(`add_ons.changelog.${addon.name.toSnakeCase()}`);
+
+ break;
+ }
+ }
+
+ // Default Icon
+ if ( ! icon )
+ icon = 'https://cdn.frankerfacez.com/badge/2/4/solid';
+ }
+ }
+
+ if ( this.addon ) {
+ icon = null;
+ title = null;
+ }
out.push({
+ icon,
title,
+ title_nav,
+ version,
author,
message,
+ segments,
active,
hash: commit.sha && commit.sha.slice(0,7),
link: commit.html_url,
@@ -177,6 +325,11 @@ export default {
},
methods: {
+ titleNav(nav) {
+ if ( Array.isArray(nav) )
+ this.$emit('navigate', ...nav);
+ },
+
formatDate(value) {
if ( ! value )
return '';
@@ -197,8 +350,15 @@ export default {
this.loading = true;
+ const url = new URL(`https://api.github.com/repos/frankerfacez/${this.addons ? 'add-ons' : 'frankerfacez'}/commits`);
+ if ( until )
+ url.searchParams.append('until', until);
+
+ if ( this.addon )
+ url.searchParams.append('path', `src/${this.addon.id}`);
+
try {
- const resp = await fetch(`https://api.github.com/repos/frankerfacez/${this.addons ? 'add-ons' : 'frankerfacez'}/commits${until ? `?until=${until}` : ''}`),
+ const resp = await fetch(url),
data = resp.ok ? await resp.json() : null;
if ( ! data || ! Array.isArray(data) ) {
diff --git a/src/modules/main_menu/components/experiments.vue b/src/modules/main_menu/components/experiments.vue
index 5f2dffa5..bb455f8c 100644
--- a/src/modules/main_menu/components/experiments.vue
+++ b/src/modules/main_menu/components/experiments.vue
@@ -252,12 +252,15 @@ export default {
},
visible_ffz() {
- const items = this.sorted_ffz,
- f = this.filter && this.filter.toLowerCase();
- if ( ! f )
- return items;
+ let out = this.sorted_ffz;
- return items.filter(x => matches(x, f));
+ if ( this.filter?.query )
+ out = out.filter(x => matches(x, this.filter.query));
+
+ if ( this.filter?.flags && this.filter.flags.has('modified') )
+ out = out.filter(x => this.item.hasOverride(x.key));
+
+ return out;
},
sorted_twitch() {
@@ -265,12 +268,15 @@ export default {
},
visible_twitch() {
- const items = this.sorted_twitch,
- f = this.filter && this.filter.toLowerCase();
- if ( ! f )
- return items;
+ let out = this.sorted_twitch;
- return items.filter(x => matches(x, f));
+ if ( this.filter?.query )
+ out = out.filter(x => matches(x, this.filter.query));
+
+ if ( this.filter?.flags && this.filter.flags.has('modified') )
+ out = out.filter(x => this.item.hasTwitchOverride(x.key));
+
+ return out;
}
},
diff --git a/src/modules/main_menu/components/main-menu.vue b/src/modules/main_menu/components/main-menu.vue
index fee567a6..7c2b919b 100644
--- a/src/modules/main_menu/components/main-menu.vue
+++ b/src/modules/main_menu/components/main-menu.vue
@@ -79,6 +79,7 @@
:current-item="currentItem"
:modal="nav"
:filter="filter"
+ :context="context"
@change-item="changeItem"
@mark-seen="markSeen"
@mark-expanded="markExpanded"
@@ -128,6 +129,10 @@
import displace from 'displacejs';
import { getDialogNextZ } from 'src/utilities/dialog';
+const VALID_FLAGS = [
+ 'modified'
+];
+
export default {
data() {
const out = this.$vnode.data;
@@ -139,7 +144,32 @@ export default {
computed: {
filter() {
- return this.query.toLowerCase()
+ let query = this.query.toLowerCase();
+
+ let flags = new Set;
+ query = query.replace(/(?<=^|\s)@(\S+)(?:\s+|$)/g, (match, flag, index) => {
+ if ( VALID_FLAGS.includes(flag) ) {
+ flags.add(flag);
+ return '';
+ }
+
+ return match;
+ });
+
+ query = query.trim();
+ if ( ! query.length )
+ query = null;
+
+ if ( ! flags.size )
+ flags = null;
+
+ if ( ! query && ! flags )
+ return null;
+
+ return {
+ flags,
+ query
+ }
}
},
diff --git a/src/modules/main_menu/components/menu-container.vue b/src/modules/main_menu/components/menu-container.vue
index 1b804431..329be5a7 100644
--- a/src/modules/main_menu/components/menu-container.vue
+++ b/src/modules/main_menu/components/menu-container.vue
@@ -57,11 +57,32 @@ export default {
this.$emit('navigate', ...args);
},
- shouldShow(item) {
- if ( ! this.filter || ! this.filter.length || ! item.search_terms )
+ shouldShow(item, is_walking = false) {
+ if ( ! this.filter || item.no_filter )
return true;
- return item.search_terms.includes(this.filter);
+ if ( this.filter.flags ) {
+ if ( this.filter.flags.has('modified') ) {
+ // We need to tree walk for this one.
+ if ( ! is_walking ) {
+ for(const key of ['tabs', 'contents', 'items'])
+ if ( item[key] )
+ for(const thing of item[key])
+ if ( this.shouldShow(thing) )
+ return true;
+ }
+
+ if ( ! item.setting || ! this.context.currentProfile.has(item.setting) )
+ return false;
+ }
+ }
+
+ if ( this.filter.query ) {
+ if ( ! item.search_terms || ! item.search_terms.includes(this.filter.query) )
+ return false;
+ }
+
+ return true;
}
}
}
diff --git a/src/modules/main_menu/components/menu-page.vue b/src/modules/main_menu/components/menu-page.vue
index b0b8fbf0..029f0645 100644
--- a/src/modules/main_menu/components/menu-page.vue
+++ b/src/modules/main_menu/components/menu-page.vue
@@ -149,15 +149,36 @@ export default {
},
methods: {
- shouldShow(item) {
- if ( ! this.filter || ! this.filter.length || ! item.search_terms )
+ shouldShow(item, is_walking = false) {
+ if ( ! this.filter || item.no_filter )
return true;
- return item.no_filter || item.search_terms.includes(this.filter);
+ if ( this.filter.flags ) {
+ if ( this.filter.flags.has('modified') ) {
+ // We need to tree walk for this one.
+ if ( ! is_walking ) {
+ for(const key of ['tabs', 'contents', 'items'])
+ if ( item[key] )
+ for(const thing of item[key])
+ if ( this.shouldShow(thing) )
+ return true;
+ }
+
+ if ( ! item.setting || ! this.context.currentProfile.has(item.setting) )
+ return false;
+ }
+ }
+
+ if ( this.filter.query ) {
+ if ( ! item.search_terms || ! item.search_terms.includes(this.filter.query) )
+ return false;
+ }
+
+ return true;
},
countMatches(item, seen) {
- if ( ! this.filter || ! this.filter.length || ! item )
+ if ( ! this.filter || ! item )
return 0;
if ( seen && seen.has(item) )
@@ -175,7 +196,7 @@ export default {
for(const thing of item[key])
count += this.countMatches(thing, seen);
- if ( item.setting && item.search_terms && item.search_terms.includes(this.filter) )
+ if ( item.setting && this.shouldShow(item, true) )
count++;
return count;
diff --git a/src/modules/main_menu/components/menu-tree.vue b/src/modules/main_menu/components/menu-tree.vue
index 2bef5f5d..9178239d 100644
--- a/src/modules/main_menu/components/menu-tree.vue
+++ b/src/modules/main_menu/components/menu-tree.vue
@@ -13,7 +13,7 @@
@@ -26,7 +26,7 @@
>