mirror of
https://github.com/ApfelTeeSaft/R2-Explorer.git
synced 2026-08-26 19:33:39 +00:00
Add infinite scroll to emails list
This commit is contained in:
@@ -176,6 +176,15 @@ export const apiHandler = {
|
||||
onUploadProgress: callback
|
||||
})
|
||||
},
|
||||
listObjects: async (bucket, prefix, delimiter = '/', cursor = null) => {
|
||||
return await api.get(`/buckets/${bucket}?include=customMetadata&include=httpMetadata`, {
|
||||
params: {
|
||||
delimiter: delimiter,
|
||||
prefix: prefix && prefix !== '/' ? encode(prefix) : '',
|
||||
cursor: cursor
|
||||
}
|
||||
})
|
||||
},
|
||||
fetchFile: async (bucket, prefix, delimiter = '/') => {
|
||||
const mainStore = useMainStore();
|
||||
let truncated = true
|
||||
@@ -184,13 +193,7 @@ export const apiHandler = {
|
||||
let contentFolders = []
|
||||
|
||||
while (truncated) {
|
||||
const response = await api.get(`/buckets/${bucket}?include=customMetadata&include=httpMetadata`, {
|
||||
params: {
|
||||
delimiter: delimiter,
|
||||
prefix: prefix && prefix !== '/' ? encode(prefix) : '',
|
||||
cursor: cursor
|
||||
}
|
||||
})
|
||||
const response = await this.listObjects(bucket, prefix, delimiter, cursor)
|
||||
|
||||
truncated = response.data.truncated
|
||||
cursor = response.data.cursor
|
||||
|
||||
@@ -131,10 +131,14 @@ export default {
|
||||
let uploadCount = 0
|
||||
for (const [folder, files] of Object.entries(folders)) {
|
||||
let targetFolder = this.selectedFolder + folder
|
||||
if (targetFolder.slice(-1) !== '/') {
|
||||
if (targetFolder.length > 0 && targetFolder.slice(-1) !== '/') {
|
||||
targetFolder += '/'
|
||||
}
|
||||
|
||||
if(targetFolder === '/' || targetFolder === ROOT_FOLDER) {
|
||||
targetFolder = ''
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
uploadCount += 1
|
||||
const key = targetFolder + file.name
|
||||
|
||||
@@ -1,75 +1,90 @@
|
||||
<template>
|
||||
<q-page class="">
|
||||
<div class="q-pa-md">
|
||||
<q-table
|
||||
ref="table"
|
||||
:rows="rows"
|
||||
:columns="columns"
|
||||
row-key="name"
|
||||
:loading="loading"
|
||||
:hide-pagination="true"
|
||||
:rows-per-page-options="[0]"
|
||||
:flat="true"
|
||||
@row-click="rowClick">
|
||||
<q-infinite-scroll ref="infScroll" :disable="loadMoreAutomatically" @load="loadNextPage" :offset="250" :debounce="100">
|
||||
<q-table
|
||||
ref="table"
|
||||
:rows="rows"
|
||||
:columns="columns"
|
||||
row-key="name"
|
||||
:loading="loading"
|
||||
:hide-pagination="true"
|
||||
:rows-per-page-options="[0]"
|
||||
:flat="true"
|
||||
@row-click="rowClick">
|
||||
|
||||
<template v-slot:loading>
|
||||
<div class="full-width q-my-lg">
|
||||
<h6 class="flex items-center justify-center">
|
||||
<q-spinner
|
||||
color="primary"
|
||||
size="xl"
|
||||
/>
|
||||
</h6>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:no-data>
|
||||
<div class="full-width q-my-lg" v-if="!loading">
|
||||
<h6 class="flex items-center justify-center">
|
||||
<q-icon name="alternate_email" color="orange" size="lg" />
|
||||
This bucket doesn't have Emails
|
||||
</h6>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:body-cell-has_attachments="prop">
|
||||
<td>
|
||||
<q-icon v-if="prop.row.has_attachments" name="attachment" size="sm" color="black" />
|
||||
</td>
|
||||
</template>
|
||||
|
||||
<template v-slot:body-cell-options="">
|
||||
<td class="text-right">
|
||||
<q-btn round flat icon="more_vert" size="sm">
|
||||
<q-menu>
|
||||
<q-list style="min-width: 100px">
|
||||
<q-item clickable v-close-popup>
|
||||
<q-item-section>New tab</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup>
|
||||
<q-item-section>New incognito tab</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
<q-item clickable v-close-popup>
|
||||
<q-item-section>Recent tabs</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup>
|
||||
<q-item-section>History</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup>
|
||||
<q-item-section>Downloads</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
<q-item clickable v-close-popup>
|
||||
<q-item-section>Settings</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
<q-item clickable v-close-popup>
|
||||
<q-item-section>Help & Feedback</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
</td>
|
||||
</template>
|
||||
</q-table>
|
||||
<template v-if="!hasMorePages">
|
||||
<div class="row justify-center q-my-md">
|
||||
<span>No more emails to load</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:loading>
|
||||
<div class="full-width q-my-lg">
|
||||
<h6 class="flex items-center justify-center">
|
||||
<q-spinner
|
||||
color="primary"
|
||||
size="xl"
|
||||
/>
|
||||
</h6>
|
||||
<div class="row justify-center q-my-md">
|
||||
<q-spinner-dots color="primary" size="40px" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:no-data>
|
||||
<div class="full-width q-my-lg" v-if="!loading">
|
||||
<h6 class="flex items-center justify-center"><q-icon name="alternate_email" color="orange" size="lg" />This bucket doesn't have Emails</h6>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:body-cell-has_attachments="prop">
|
||||
<td>
|
||||
<q-icon v-if="prop.row.has_attachments" name="attachment" size="sm" color="black" />
|
||||
</td>
|
||||
</template>
|
||||
|
||||
<template v-slot:body-cell-options="">
|
||||
<td class="text-right">
|
||||
<q-btn round flat icon="more_vert" size="sm">
|
||||
<q-menu>
|
||||
<q-list style="min-width: 100px">
|
||||
<q-item clickable v-close-popup>
|
||||
<q-item-section>New tab</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup>
|
||||
<q-item-section>New incognito tab</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
<q-item clickable v-close-popup>
|
||||
<q-item-section>Recent tabs</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup>
|
||||
<q-item-section>History</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup>
|
||||
<q-item-section>Downloads</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
<q-item clickable v-close-popup>
|
||||
<q-item-section>Settings</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
<q-item clickable v-close-popup>
|
||||
<q-item-section>Help & Feedback</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
</td>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-infinite-scroll>
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
@@ -78,116 +93,196 @@
|
||||
import { defineComponent } from "vue";
|
||||
import { api } from "boot/axios";
|
||||
import { useMainStore } from "stores/main-store";
|
||||
import { decode, encode, timeSince } from "../../appUtils";
|
||||
import { apiHandler, encode, timeSince } from "../../appUtils";
|
||||
|
||||
export default defineComponent({
|
||||
name: 'EmailFolderPage',
|
||||
data: function () {
|
||||
name: "EmailFolderPage",
|
||||
data: function() {
|
||||
return {
|
||||
indexCursors: null,
|
||||
loading: false,
|
||||
loadMoreAutomatically: true,
|
||||
hasMorePages: true,
|
||||
rows: [],
|
||||
columns: [
|
||||
{
|
||||
name: 'sender',
|
||||
name: "sender",
|
||||
required: true,
|
||||
field: 'sender',
|
||||
align: 'left',
|
||||
field: "sender",
|
||||
align: "left",
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
name: 'subject',
|
||||
name: "subject",
|
||||
required: true,
|
||||
field: 'subject',
|
||||
align: 'left',
|
||||
field: "subject",
|
||||
align: "left",
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
name: 'has_attachments',
|
||||
name: "has_attachments",
|
||||
required: true,
|
||||
align: 'left',
|
||||
field: 'has_attachments',
|
||||
sortable: false,
|
||||
align: "left",
|
||||
field: "has_attachments",
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
name: 'lastModified',
|
||||
name: "lastModified",
|
||||
required: true,
|
||||
align: 'left',
|
||||
field: 'lastModified',
|
||||
sortable: false,
|
||||
},
|
||||
align: "left",
|
||||
field: "lastModified",
|
||||
sortable: false
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
selectedBucket: function () {
|
||||
return this.$route.params.bucket
|
||||
selectedBucket: function() {
|
||||
return this.$route.params.bucket;
|
||||
},
|
||||
selectedFolder: function() {
|
||||
return "inbox";
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedBucket(newVal) {
|
||||
this.fetchFiles()
|
||||
},
|
||||
this.fetchFiles();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
rowClick: function(evt, row, index) {
|
||||
const file = row.key.replace(/^.*[\\/]/, '')
|
||||
const file = row.key.replace(/^.*[\\/]/, "");
|
||||
// const folder = row.key.replace(file, '')
|
||||
|
||||
this.$router.push({ name: `email-file`, params: { bucket: this.selectedBucket, folder: 'inbox', file: encode(file) }})
|
||||
this.$router.push({
|
||||
name: `email-file`,
|
||||
params: { bucket: this.selectedBucket, folder: this.selectedFolder, file: encode(file) }
|
||||
});
|
||||
},
|
||||
fetchFiles: async function () {
|
||||
const self = this
|
||||
this.loading = true
|
||||
let truncated = true
|
||||
let cursor = null
|
||||
let contentFiles = []
|
||||
createOrUpdateIndex: async function(currentIndex) {
|
||||
let truncated = true;
|
||||
|
||||
while (truncated) {
|
||||
const response = await api.get(`/buckets/${this.selectedBucket}?include=customMetadata&include=httpMetadata`, {
|
||||
params: {
|
||||
delimiter: '/',
|
||||
prefix: encode('.r2-explorer/emails/inbox/'),
|
||||
cursor: cursor
|
||||
}
|
||||
})
|
||||
// fallback when there's no previous index
|
||||
let cursor = null;
|
||||
let pageNum = 0;
|
||||
let indexData = {
|
||||
version: 1,
|
||||
cursors: []
|
||||
};
|
||||
|
||||
truncated = response.data.truncated
|
||||
cursor = response.data.cursor
|
||||
|
||||
if (response.data.objects) {
|
||||
const files = response.data.objects.filter(function(obj) {
|
||||
return !obj.key.endsWith('/') // Remove selected folder
|
||||
}).map(function(obj) {
|
||||
const date = new Date(obj.uploaded)
|
||||
|
||||
return {
|
||||
...obj,
|
||||
sender: obj.customMetadata.from_name || obj.customMetadata.from_address,
|
||||
subject: obj.customMetadata.subject,
|
||||
has_attachments: obj.customMetadata.has_attachments === 'true',
|
||||
read: obj.customMetadata.read,
|
||||
lastModified: timeSince(date),
|
||||
timestamp: date.getTime(),
|
||||
}
|
||||
})
|
||||
|
||||
for (const f of files) {
|
||||
contentFiles.push(f)
|
||||
}
|
||||
if (currentIndex) {
|
||||
// There is a previous index
|
||||
indexData = currentIndex;
|
||||
if (currentIndex.cursors.length > 0) {
|
||||
// Pop last page, as its going to be updated
|
||||
const lastPage = currentIndex.cursors.pop();
|
||||
pageNum = lastPage.page;
|
||||
cursor = lastPage.cursor;
|
||||
}
|
||||
}
|
||||
|
||||
this.rows = contentFiles
|
||||
this.loading = false
|
||||
while (truncated) {
|
||||
console.log(`Updating index page ${pageNum}`);
|
||||
const response = await api.get(`/buckets/${this.selectedBucket}?include=customMetadata&include=httpMetadata`, {
|
||||
params: {
|
||||
delimiter: "/",
|
||||
prefix: encode(`.r2-explorer/emails/${this.selectedFolder}/`),
|
||||
cursor: cursor
|
||||
}
|
||||
});
|
||||
|
||||
indexData.cursors.push({
|
||||
page: pageNum,
|
||||
cursor: cursor,
|
||||
items: response.data.objects.length
|
||||
});
|
||||
|
||||
// update cursor for next page
|
||||
cursor = response.data.cursor;
|
||||
truncated = response.data.truncated;
|
||||
pageNum++;
|
||||
}
|
||||
|
||||
return indexData;
|
||||
},
|
||||
getOrCreateIndex: async function() {
|
||||
const indexKey = `.r2-explorer/emails/index-${this.selectedFolder}.json`;
|
||||
const fileData = await apiHandler.downloadFile(this.selectedBucket, indexKey, {}).then((obj) => obj.data).catch((obj) => null);
|
||||
|
||||
const updatedIndex = await this.createOrUpdateIndex(fileData);
|
||||
|
||||
const blob = new Blob([JSON.stringify(updatedIndex)], {
|
||||
type: "application/json"
|
||||
});
|
||||
await apiHandler.uploadObjects(blob, indexKey, this.selectedBucket);
|
||||
|
||||
return updatedIndex;
|
||||
},
|
||||
loadNextPage: async function(index, done) {
|
||||
const page = this.indexCursors[index]
|
||||
|
||||
if (page) {
|
||||
await this.loadIndexPage(page)
|
||||
} else {
|
||||
// No more pages to load
|
||||
this.loadMoreAutomatically = true
|
||||
this.hasMorePages = false
|
||||
}
|
||||
|
||||
done()
|
||||
},
|
||||
fetchFiles: async function() {
|
||||
this.loading = true;
|
||||
|
||||
const indexData = await this.getOrCreateIndex();
|
||||
|
||||
this.indexCursors = indexData.cursors.reverse()
|
||||
|
||||
await this.$refs.infScroll.setIndex(-1) // First page is 0
|
||||
await this.$refs.infScroll.trigger()
|
||||
|
||||
this.loadMoreAutomatically = false
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
loadIndexPage: async function(page) {
|
||||
const response = await apiHandler.listObjects(
|
||||
this.selectedBucket,
|
||||
`.r2-explorer/emails/${this.selectedFolder}/`,
|
||||
"/",
|
||||
page.cursor
|
||||
);
|
||||
|
||||
if (response.data.objects) {
|
||||
const files = response.data.objects.filter(function(obj) {
|
||||
return !obj.key.endsWith("/"); // Remove selected folder
|
||||
}).map(function(obj) {
|
||||
const date = new Date(obj.uploaded);
|
||||
|
||||
return {
|
||||
...obj,
|
||||
sender: obj.customMetadata.from_name || obj.customMetadata.from_address,
|
||||
subject: obj.customMetadata.subject,
|
||||
has_attachments: obj.customMetadata.has_attachments === "true",
|
||||
read: obj.customMetadata.read,
|
||||
lastModified: timeSince(date),
|
||||
timestamp: date.getTime()
|
||||
};
|
||||
});
|
||||
|
||||
for (const f of files.reverse()) {
|
||||
this.rows.push(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchFiles()
|
||||
this.fetchFiles();
|
||||
},
|
||||
setup () {
|
||||
setup() {
|
||||
return {
|
||||
mainStore: useMainStore()
|
||||
}
|
||||
},
|
||||
})
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user