-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Google photos #5061
base: main
Are you sure you want to change the base?
Google photos #5061
Conversation
in companion - make one authProvider for each of drive/googlephotos, because reusing would have been a big rewrite - allow the provider.download method to optionally return a size as an optimization
Diff output filesdiff --git a/packages/@uppy/box/lib/Box.js b/packages/@uppy/box/lib/Box.js
index e45f972..1122bd4 100644
--- a/packages/@uppy/box/lib/Box.js
+++ b/packages/@uppy/box/lib/Box.js
@@ -58,6 +58,7 @@ export default class Box extends UIPlugin {
this.view = new ProviderViews(this, {
provider: this.provider,
loadAllFiles: true,
+ virtualList: true,
});
const {
target,
diff --git a/packages/@uppy/companion/lib/config/grant.d.ts b/packages/@uppy/companion/lib/config/grant.d.ts
index f7df73c..97c46b1 100644
--- a/packages/@uppy/companion/lib/config/grant.d.ts
+++ b/packages/@uppy/companion/lib/config/grant.d.ts
@@ -1,12 +1,29 @@
declare function _exports(): {
- google: {
- transport: string;
+ googledrive: {
+ callback: string;
scope: string[];
+ transport: string;
+ custom_params: {
+ access_type: string;
+ prompt: string;
+ };
+ authorize_url: string;
+ access_url: string;
+ oauth: number;
+ scope_delimiter: string;
+ };
+ googlephotos: {
callback: string;
+ scope: string[];
+ transport: string;
custom_params: {
access_type: string;
prompt: string;
};
+ authorize_url: string;
+ access_url: string;
+ oauth: number;
+ scope_delimiter: string;
};
dropbox: {
transport: string;
diff --git a/packages/@uppy/companion/lib/config/grant.js b/packages/@uppy/companion/lib/config/grant.js
index 861adf7..6f113e3 100644
--- a/packages/@uppy/companion/lib/config/grant.js
+++ b/packages/@uppy/companion/lib/config/grant.js
@@ -1,21 +1,36 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
+const google = {
+ transport: "session",
+ // access_type: offline is needed in order to get refresh tokens.
+ // prompt: 'consent' is needed because sometimes a user will get stuck in an authenticated state where we will
+ // receive no refresh tokens from them. This seems to be happen when running on different subdomains.
+ // therefore to be safe that we always get refresh tokens, we set this.
+ // https://stackoverflow.com/questions/10827920/not-receiving-google-oauth-refresh-token/65108513#65108513
+ custom_params: { access_type: "offline", prompt: "consent" },
+ // copied from https://github.com/simov/grant/blob/master/config/oauth.json
+ "authorize_url": "https://accounts.google.com/o/oauth2/v2/auth",
+ "access_url": "https://oauth2.googleapis.com/token",
+ "oauth": 2,
+ "scope_delimiter": " ",
+};
// oauth configuration for provider services that are used.
module.exports = () => {
return {
- // for drive
- google: {
- transport: "session",
- scope: [
- "https://www.googleapis.com/auth/drive.readonly",
- ],
+ // we need separate auth providers because scopes are different,
+ // and because it would be a too big rewrite to allow reuse of the same provider.
+ googledrive: {
+ ...google,
callback: "/drive/callback",
- // access_type: offline is needed in order to get refresh tokens.
- // prompt: 'consent' is needed because sometimes a user will get stuck in an authenticated state where we will
- // receive no refresh tokens from them. This seems to be happen when running on different subdomains.
- // therefore to be safe that we always get refresh tokens, we set this.
- // https://stackoverflow.com/questions/10827920/not-receiving-google-oauth-refresh-token/65108513#65108513
- custom_params: { access_type: "offline", prompt: "consent" },
+ scope: ["https://www.googleapis.com/auth/drive.readonly"],
+ },
+ googlephotos: {
+ ...google,
+ callback: "/googlephotos/callback",
+ scope: [
+ "https://www.googleapis.com/auth/photoslibrary.readonly",
+ "https://www.googleapis.com/auth/userinfo.email",
+ ], // if name is needed, then add https://www.googleapis.com/auth/userinfo.profile too
},
dropbox: {
transport: "session",
diff --git a/packages/@uppy/companion/lib/server/controllers/get.js b/packages/@uppy/companion/lib/server/controllers/get.js
index c3b5129..44c2d07 100644
--- a/packages/@uppy/companion/lib/server/controllers/get.js
+++ b/packages/@uppy/companion/lib/server/controllers/get.js
@@ -10,10 +10,7 @@ async function get(req, res) {
async function getSize() {
return provider.size({ id, token: accessToken, query: req.query });
}
- async function download() {
- const { stream } = await provider.download({ id, token: accessToken, providerUserSession, query: req.query });
- return stream;
- }
+ const download = () => provider.download({ id, token: accessToken, providerUserSession, query: req.query });
try {
await startDownUpload({ req, res, getSize, download });
} catch (err) {
diff --git a/packages/@uppy/companion/lib/server/controllers/url.js b/packages/@uppy/companion/lib/server/controllers/url.js
index 4ab5695..9c36bd4 100644
--- a/packages/@uppy/companion/lib/server/controllers/url.js
+++ b/packages/@uppy/companion/lib/server/controllers/url.js
@@ -26,8 +26,8 @@ const downloadURL = async (url, blockLocalIPs, traceId) => {
try {
const protectedGot = getProtectedGot({ blockLocalIPs });
const stream = protectedGot.stream.get(url, { responseType: "json" });
- await prepareStream(stream);
- return stream;
+ const { size } = await prepareStream(stream);
+ return { stream, size };
} catch (err) {
logger.error(err, "controller.url.download.error", traceId);
throw err;
@@ -73,9 +73,7 @@ const get = async (req, res) => {
const { size } = await getURLMeta(req.body.url, !allowLocalUrls);
return size;
}
- async function download() {
- return downloadURL(req.body.url, !allowLocalUrls, req.id);
- }
+ const download = () => downloadURL(req.body.url, !allowLocalUrls, req.id);
try {
await startDownUpload({ req, res, getSize, download });
} catch (err) {
diff --git a/packages/@uppy/companion/lib/server/helpers/oauth-state.d.ts b/packages/@uppy/companion/lib/server/helpers/oauth-state.d.ts
index f1c2709..f2b204d 100644
--- a/packages/@uppy/companion/lib/server/helpers/oauth-state.d.ts
+++ b/packages/@uppy/companion/lib/server/helpers/oauth-state.d.ts
@@ -1,4 +1,5 @@
export function encodeState(state: any, secret: any): string;
+export function decodeState(state: any, secret: any): any;
export function generateState(): {
id: string;
};
diff --git a/packages/@uppy/companion/lib/server/helpers/oauth-state.js b/packages/@uppy/companion/lib/server/helpers/oauth-state.js
index ccdc572..9421cec 100644
--- a/packages/@uppy/companion/lib/server/helpers/oauth-state.js
+++ b/packages/@uppy/companion/lib/server/helpers/oauth-state.js
@@ -7,7 +7,7 @@ module.exports.encodeState = (state, secret) => {
const encodedState = Buffer.from(JSON.stringify(state)).toString("base64");
return encrypt(encodedState, secret);
};
-const decodeState = (state, secret) => {
+module.exports.decodeState = (state, secret) => {
const encodedState = decrypt(state, secret);
return JSON.parse(atob(encodedState));
};
@@ -17,7 +17,7 @@ module.exports.generateState = () => {
};
};
module.exports.getFromState = (state, name, secret) => {
- return decodeState(state, secret)[name];
+ return module.exports.decodeState(state, secret)[name];
};
module.exports.getGrantDynamicFromRequest = (req) => {
var _a, _b;
diff --git a/packages/@uppy/companion/lib/server/helpers/upload.js b/packages/@uppy/companion/lib/server/helpers/upload.js
index 923af2a..948e8b4 100644
--- a/packages/@uppy/companion/lib/server/helpers/upload.js
+++ b/packages/@uppy/companion/lib/server/helpers/upload.js
@@ -5,12 +5,20 @@ const logger = require("../logger");
const { respondWithError } = require("../provider/error");
async function startDownUpload({ req, res, getSize, download }) {
try {
- const size = await getSize();
+ logger.debug("Starting download stream.", null, req.id);
+ const { stream, size: maybeSize } = await download();
+ let size;
+ // if the provider already knows the size, we can use that
+ if (typeof maybeSize === "number" && !Number.isNaN(maybeSize) && maybeSize > 0) {
+ size = maybeSize;
+ }
+ // if not we need to get the size
+ if (size == null) {
+ size = await getSize();
+ }
const { clientSocketConnectTimeout } = req.companion.options;
logger.debug("Instantiating uploader.", null, req.id);
const uploader = new Uploader(Uploader.reqToOptions(req, size));
- logger.debug("Starting download stream.", null, req.id);
- const stream = await download();
(async () => {
// wait till the client has connected to the socket, before starting
// the download, so that the client can receive all download/upload progress.
diff --git a/packages/@uppy/companion/lib/server/helpers/utils.js b/packages/@uppy/companion/lib/server/helpers/utils.js
index 5b2a4a7..b7b58e4 100644
--- a/packages/@uppy/companion/lib/server/helpers/utils.js
+++ b/packages/@uppy/companion/lib/server/helpers/utils.js
@@ -140,11 +140,14 @@ module.exports.StreamHttpJsonError = StreamHttpJsonError;
module.exports.prepareStream = async (stream) =>
new Promise((resolve, reject) => {
stream
- .on("response", () => {
+ .on("response", (response) => {
+ const contentLengthStr = response.headers["content-length"];
+ const contentLength = parseInt(contentLengthStr, 10);
+ const size = !Number.isNaN(contentLength) && contentLength >= 0 ? contentLength : undefined;
// Don't allow any more data to flow yet.
// https://github.com/request/request/issues/1990#issuecomment-184712275
stream.pause();
- resolve();
+ resolve({ size });
})
.on("error", (err) => {
var _a, _b;
diff --git a/packages/@uppy/companion/lib/server/provider/index.js b/packages/@uppy/companion/lib/server/provider/index.js
index de030b5..3c7caea 100644
--- a/packages/@uppy/companion/lib/server/provider/index.js
+++ b/packages/@uppy/companion/lib/server/provider/index.js
@@ -5,7 +5,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
*/
const dropbox = require("./dropbox");
const box = require("./box");
-const drive = require("./drive");
+const drive = require("./google/drive");
+const googlephotos = require("./google/googlephotos");
const instagram = require("./instagram/graph");
const facebook = require("./facebook");
const onedrive = require("./onedrive");
@@ -63,7 +64,7 @@ module.exports.getProviderMiddleware = (providers, grantConfig) => {
* @returns {Record<string, typeof Provider>}
*/
module.exports.getDefaultProviders = () => {
- const providers = { dropbox, box, drive, facebook, onedrive, zoom, instagram, unsplash };
+ const providers = { dropbox, box, drive, googlephotos, facebook, onedrive, zoom, instagram, unsplash };
return providers;
};
/**
diff --git a/packages/@uppy/companion/lib/server/provider/providerErrors.d.ts b/packages/@uppy/companion/lib/server/provider/providerErrors.d.ts
index c8b3753..9e37f86 100644
--- a/packages/@uppy/companion/lib/server/provider/providerErrors.d.ts
+++ b/packages/@uppy/companion/lib/server/provider/providerErrors.d.ts
@@ -24,3 +24,4 @@ export function withProviderErrorHandling({ fn, tag, providerName, isAuthError,
}) => boolean;
getJsonErrorMessage: (a: object) => string;
}): Promise<any>;
+export function withGoogleErrorHandling(providerName: any, tag: any, fn: any): Promise<any>;
diff --git a/packages/@uppy/companion/lib/server/provider/providerErrors.js b/packages/@uppy/companion/lib/server/provider/providerErrors.js
index 1595560..9196c01 100644
--- a/packages/@uppy/companion/lib/server/provider/providerErrors.js
+++ b/packages/@uppy/companion/lib/server/provider/providerErrors.js
@@ -57,4 +57,25 @@ async function withProviderErrorHandling(
throw err;
}
}
-module.exports = { withProviderErrorHandling };
+async function withGoogleErrorHandling(providerName, tag, fn) {
+ return withProviderErrorHandling({
+ fn,
+ tag,
+ providerName,
+ isAuthError: (response) => {
+ var _a;
+ return (
+ response.statusCode === 401
+ || (response.statusCode === 400
+ && ((_a = response.body) === null || _a === void 0 ? void 0 : _a.error) === "invalid_grant") // Refresh token has expired or been revoked
+ );
+ },
+ getJsonErrorMessage: (body) => {
+ var _a;
+ return (_a = body === null || body === void 0 ? void 0 : body.error) === null || _a === void 0
+ ? void 0
+ : _a.message;
+ },
+ });
+}
+module.exports = { withProviderErrorHandling, withGoogleErrorHandling };
diff --git a/packages/@uppy/companion/lib/standalone/helper.js b/packages/@uppy/companion/lib/standalone/helper.js
index 9c21b17..6a83a86 100644
--- a/packages/@uppy/companion/lib/standalone/helper.js
+++ b/packages/@uppy/companion/lib/standalone/helper.js
@@ -72,6 +72,11 @@ const getConfigFromEnv = () => {
secret: getSecret("COMPANION_GOOGLE_SECRET"),
credentialsURL: process.env.COMPANION_GOOGLE_KEYS_ENDPOINT,
},
+ googlephotos: {
+ key: process.env.COMPANION_GOOGLE_KEY,
+ secret: getSecret("COMPANION_GOOGLE_SECRET"),
+ credentialsURL: process.env.COMPANION_GOOGLE_KEYS_ENDPOINT,
+ },
dropbox: {
key: process.env.COMPANION_DROPBOX_KEY,
secret: getSecret("COMPANION_DROPBOX_SECRET"),
diff --git a/packages/@uppy/dashboard/lib/utils/copyToClipboard.js b/packages/@uppy/dashboard/lib/utils/copyToClipboard.js
index ed94e73..05bf18e 100644
--- a/packages/@uppy/dashboard/lib/utils/copyToClipboard.js
+++ b/packages/@uppy/dashboard/lib/utils/copyToClipboard.js
@@ -19,7 +19,7 @@ export default function copyToClipboard(textToCopy, fallbackString) {
textArea.value = textToCopy;
document.body.appendChild(textArea);
textArea.select();
- const magicCopyFailed = cause => {
+ const magicCopyFailed = () => {
document.body.removeChild(textArea);
window.prompt(fallbackString, textToCopy);
resolve();
@@ -27,13 +27,13 @@ export default function copyToClipboard(textToCopy, fallbackString) {
try {
const successful = document.execCommand("copy");
if (!successful) {
- return magicCopyFailed("copy command unavailable");
+ return magicCopyFailed();
}
document.body.removeChild(textArea);
return resolve();
} catch (err) {
document.body.removeChild(textArea);
- return magicCopyFailed(err);
+ return magicCopyFailed();
}
});
}
diff --git a/packages/@uppy/dropbox/lib/Dropbox.js b/packages/@uppy/dropbox/lib/Dropbox.js
index 3a5bae8..76d0e03 100644
--- a/packages/@uppy/dropbox/lib/Dropbox.js
+++ b/packages/@uppy/dropbox/lib/Dropbox.js
@@ -50,6 +50,7 @@ export default class Dropbox extends UIPlugin {
this.view = new ProviderViews(this, {
provider: this.provider,
loadAllFiles: true,
+ virtualList: true,
});
const {
target,
diff --git a/packages/@uppy/google-drive/lib/GoogleDrive.js b/packages/@uppy/google-drive/lib/GoogleDrive.js
index 1269a20..14c8f7e 100644
--- a/packages/@uppy/google-drive/lib/GoogleDrive.js
+++ b/packages/@uppy/google-drive/lib/GoogleDrive.js
@@ -75,6 +75,7 @@ export default class GoogleDrive extends UIPlugin {
this.view = new DriveProviderViews(this, {
provider: this.provider,
loadAllFiles: true,
+ virtualList: true,
});
const {
target,
diff --git a/packages/@uppy/onedrive/lib/OneDrive.js b/packages/@uppy/onedrive/lib/OneDrive.js
index edec3e4..86c3896 100644
--- a/packages/@uppy/onedrive/lib/OneDrive.js
+++ b/packages/@uppy/onedrive/lib/OneDrive.js
@@ -67,6 +67,7 @@ export default class OneDrive extends UIPlugin {
this.view = new ProviderViews(this, {
provider: this.provider,
loadAllFiles: true,
+ virtualList: true,
});
const {
target,
diff --git a/packages/@uppy/provider-views/lib/Browser.js b/packages/@uppy/provider-views/lib/Browser.js
index 60ad2f8..06876c0 100644
--- a/packages/@uppy/provider-views/lib/Browser.js
+++ b/packages/@uppy/provider-views/lib/Browser.js
@@ -43,7 +43,7 @@ function ListItem(props) {
id: f.id,
title: f.name,
author: f.author,
- getItemIcon: () => f.icon,
+ getItemIcon: () => viewType === "grid" && f.thumbnail ? f.thumbnail : f.icon,
isChecked: isChecked(f),
toggleCheckbox: event => toggleCheckbox(event, f),
isCheckboxDisabled: false,
@@ -84,7 +84,7 @@ function Browser(props) {
cancel,
done,
noResultsLabel,
- loadAllFiles,
+ virtualList,
} = props;
const selected = currentSelection.length;
const rows = useMemo(() => [...folders, ...files], [folders, files]);
@@ -131,7 +131,7 @@ function Browser(props) {
className: "uppy-Provider-empty",
}, noResultsLabel);
}
- if (loadAllFiles) {
+ if (virtualList) {
return h(
"div",
{
diff --git a/packages/@uppy/provider-views/lib/ProviderView/ProviderView.js b/packages/@uppy/provider-views/lib/ProviderView/ProviderView.js
index 33f6b85..19d6936 100644
--- a/packages/@uppy/provider-views/lib/ProviderView/ProviderView.js
+++ b/packages/@uppy/provider-views/lib/ProviderView/ProviderView.js
@@ -47,6 +47,7 @@ const defaultOptions = {
showFilter: true,
showBreadcrumbs: true,
loadAllFiles: false,
+ virtualList: false,
};
var _abortController = _classPrivateFieldLooseKey("abortController");
var _withAbort = _classPrivateFieldLooseKey("withAbort");
@@ -386,6 +387,7 @@ export default class ProviderView extends View {
getNextFolder: this.getNextFolder,
getFolder: this.getFolder,
loadAllFiles: this.opts.loadAllFiles,
+ virtualList: this.opts.virtualList,
showSearchFilter: targetViewOptions.showFilter,
search: this.filterQuery,
clearSearch: this.clearFilter,
diff --git a/packages/@uppy/provider-views/lib/View.js b/packages/@uppy/provider-views/lib/View.js
index 69cd639..0886ac5 100644
--- a/packages/@uppy/provider-views/lib/View.js
+++ b/packages/@uppy/provider-views/lib/View.js
@@ -1,5 +1,3 @@
-import getFileType from "@uppy/utils/lib/getFileType";
-import isPreviewSupported from "@uppy/utils/lib/isPreviewSupported";
import remoteFileObjToLocal from "@uppy/utils/lib/remoteFileObjToLocal";
export default class View {
constructor(plugin, opts) {
@@ -104,8 +102,7 @@ export default class View {
requestClientId: this.requestClientId,
},
};
- const fileType = getFileType(tagFile);
- if (fileType && isPreviewSupported(fileType)) {
+ if (file.thumbnail) {
tagFile.preview = file.thumbnail;
}
if (file.author) {
diff --git a/packages/@uppy/remote-sources/lib/index.js b/packages/@uppy/remote-sources/lib/index.js
index a8af254..17d3e0b 100644
--- a/packages/@uppy/remote-sources/lib/index.js
+++ b/packages/@uppy/remote-sources/lib/index.js
@@ -13,6 +13,7 @@ import { BasePlugin } from "@uppy/core";
import Dropbox from "@uppy/dropbox";
import Facebook from "@uppy/facebook";
import GoogleDrive from "@uppy/google-drive";
+import GooglePhotos from "@uppy/google-photos";
import Instagram from "@uppy/instagram";
import OneDrive from "@uppy/onedrive";
import Unsplash from "@uppy/unsplash";
@@ -27,6 +28,7 @@ const availablePlugins = {
Dropbox,
Facebook,
GoogleDrive,
+ GooglePhotos,
Instagram,
OneDrive,
Unsplash,
diff --git a/packages/@uppy/transloadit/lib/index.js b/packages/@uppy/transloadit/lib/index.js
index 529121d..2683802 100644
--- a/packages/@uppy/transloadit/lib/index.js
+++ b/packages/@uppy/transloadit/lib/index.js
@@ -533,6 +533,7 @@ function _getClientVersion2() {
addPluginVersion("Box", "uppy-box");
addPluginVersion("Facebook", "uppy-facebook");
addPluginVersion("GoogleDrive", "uppy-google-drive");
+ addPluginVersion("GooglePhotos", "uppy-google-photos");
addPluginVersion("Instagram", "uppy-instagram");
addPluginVersion("OneDrive", "uppy-onedrive");
addPluginVersion("Zoom", "uppy-zoom"); |
they are different concept and virtual list doesn't support grid
I think this is now ready for MVP |
This comment was marked as resolved.
This comment was marked as resolved.
hmm i ran |
# Conflicts: # packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx
as required by google
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code wise in very good shape! Thanks for picking this up 👌
Still have to check this out locally to see how it feels
packages/@uppy/companion/src/server/provider/google/googlephotos/index.js
Show resolved
Hide resolved
/** | ||
* Reusable google stuff | ||
*/ | ||
class Google extends Provider { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not the biggest fan of inheritance for this instead of just having three simple functions which are reused, but more of a nit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
me neither, but since we're already using classes, i figured we'd continue with that code style
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can just write these three functions in google drive and export them from there (or a new file). They're small and simple and some don't not even use this
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.env.example
should be updated too
not needed afaik because it uses the same env variable as for google drive |
ok i finally managed to get ts building too |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for implementing the feedback. It's a big PR, so two approvals would be nice before merge.
Actually I forgot I still need to test this locally to see the user experience. I will do that next week. |
any updates?:) would be nice to get it out on uppy.io so people can start testing |
you mean you see empty strings in the network tools json response? do you have any way of reproducing this?
you mean show a folder icon for all albums (no photo thumbnail)? |
Here is the network response. {
"username": "merlijn.vos043@gmail.com",
"items": [
{
"isFolder": true,
"icon": "https://lh3.googleusercontent.com/lr/AAJ1LKeAAPyYUFtLUYMCgwFpktAWGYMaruVbOA3RLj0e-gy7rhY-RkoM3rKwmlGxrS6ZF4ORfhbpbDiZ_A_E1a3lc15tkN27Ox1YkuV-G1ykXi91See7oFZbAHs2uO9X36lTU3-hmnUGyQV58It_OiaA9LCPuveMf2l_FGrfg8zsfuu7qEOIs3hHprqSAa90gpM-5inGIW9trBLI2oEjO3_QHRSsanYISC_R9O124n8WZkLC_SfyVCj-MrN_1UgmNWTW52L7rcOvtw1EAT6qrhK1QSZ63dgdhHwcKS0_ogqkmoCOJTHyYtoedh0wiPG_boU7zNHkvk0PbmV2OOGqzCVcoHkDiRz2RoRXVRYj-MNSQSzYFh_t0ELHO_qtJVe_AuOV5kZcGJT40HCCQw5Rnt8SL8OMX02ub5Z5gMFCcN_WePzKe_fWFIS-c00INJOvexesBzyF6Au-MlZFP6jtJrXGZv03LLDzPpAkktDo5641gFmakJm_fOSvS3NgqygAMP1ycdUvXGMqMUUCkZayj6t0Fz0MyDuGu19QgeGVRBmtwdW3vGEZ_ntSHHa6D4qtovXknVCAM-E2Iwny9KbCz4kGxEHuJV0aERCmC9tHDwOwACMSpyicY0H4ooH4x50j7V5IMC_UHlojdL0A6kx58BQBKL5-WW64MD880_d1YDb5kNlcYhpnUNlrBDn6Z6DS_6F9TFekRhdUa4vZxeSBDAx10ML5FNcg3gdhA0JnG1dDgJtiyXdlJAVlg8jHZf5dFbwA3Z_KY8VXkl-yQ2F6wX3bKBtuvoeERP-UPXUr1HcSI6W7U_o1cLdnJYyUXmtUgl1tC0WrOkHmxfRS8S8RfUfF80YR7-EeeuKPX1cg4afKzy1MlSCujYVUlr_rr7_bgKwTkyJA61YysZV687PYB_KCHvdzjZpS8JWRTO7Z_J4nQclky90nS9dW-gFDYa45dryvFTc2VD8C0TuMP0rIpYNorDr19XYZ4e-HB3jNCw=w64-h64-c",
"thumbnail": "https://lh3.googleusercontent.com/lr/AAJ1LKeAAPyYUFtLUYMCgwFpktAWGYMaruVbOA3RLj0e-gy7rhY-RkoM3rKwmlGxrS6ZF4ORfhbpbDiZ_A_E1a3lc15tkN27Ox1YkuV-G1ykXi91See7oFZbAHs2uO9X36lTU3-hmnUGyQV58It_OiaA9LCPuveMf2l_FGrfg8zsfuu7qEOIs3hHprqSAa90gpM-5inGIW9trBLI2oEjO3_QHRSsanYISC_R9O124n8WZkLC_SfyVCj-MrN_1UgmNWTW52L7rcOvtw1EAT6qrhK1QSZ63dgdhHwcKS0_ogqkmoCOJTHyYtoedh0wiPG_boU7zNHkvk0PbmV2OOGqzCVcoHkDiRz2RoRXVRYj-MNSQSzYFh_t0ELHO_qtJVe_AuOV5kZcGJT40HCCQw5Rnt8SL8OMX02ub5Z5gMFCcN_WePzKe_fWFIS-c00INJOvexesBzyF6Au-MlZFP6jtJrXGZv03LLDzPpAkktDo5641gFmakJm_fOSvS3NgqygAMP1ycdUvXGMqMUUCkZayj6t0Fz0MyDuGu19QgeGVRBmtwdW3vGEZ_ntSHHa6D4qtovXknVCAM-E2Iwny9KbCz4kGxEHuJV0aERCmC9tHDwOwACMSpyicY0H4ooH4x50j7V5IMC_UHlojdL0A6kx58BQBKL5-WW64MD880_d1YDb5kNlcYhpnUNlrBDn6Z6DS_6F9TFekRhdUa4vZxeSBDAx10ML5FNcg3gdhA0JnG1dDgJtiyXdlJAVlg8jHZf5dFbwA3Z_KY8VXkl-yQ2F6wX3bKBtuvoeERP-UPXUr1HcSI6W7U_o1cLdnJYyUXmtUgl1tC0WrOkHmxfRS8S8RfUfF80YR7-EeeuKPX1cg4afKzy1MlSCujYVUlr_rr7_bgKwTkyJA61YysZV687PYB_KCHvdzjZpS8JWRTO7Z_J4nQclky90nS9dW-gFDYa45dryvFTc2VD8C0TuMP0rIpYNorDr19XYZ4e-HB3jNCw=w300-h300-c",
"name": "Fen",
"id": "ABlQyLyVT73CwpjSmLSrFjZcbeksmvOsPusxfKEYsxKnQ55zPec1ykSk2fVdxsTN-J0Uw3LjZxt-",
"requestPath": "ABlQyLyVT73CwpjSmLSrFjZcbeksmvOsPusxfKEYsxKnQ55zPec1ykSk2fVdxsTN-J0Uw3LjZxt-"
},
{
"isFolder": true,
"icon": "https://lh3.googleusercontent.com/lr/AAJ1LKeQlMlh8_iaFrzN1yCLbafP9AkStDG83hCOdaYYht7MwwXqkWrUrNgcJ2AHfMPNo0zfdqg9zdhaLS1CTJhor0dXES7yE6U1wahFRZFt96GlX_mD2x_HpF66t0ujXgO_6-wGjbUk0iX-ckNDxV84oXu_2Cot_ywEYORxk9VF5NFavYevvlaBm6gnZnNyaT0vnicA4KcaYk_Od46IIrRMnG6Ubh2ChcEn0rOjLwquw33obr9QVDuV3vc2iHCajO9vkRLEvQEMatbO_713nSPTPm7HLTd-D3dPHBiVMZUefkJD1lH_WPwtL83N95krnrBpwg-xP6_9R2EmDgRka4rLOU-Om7cvNnrgjDzri1PQSJIqO8dAjUnMr83XfS8yLYgrESIk0v_gbgQww7GqkGBra98tgVrF-vgtH_4Hiv5QW89QcsOLSsYpOuuXikFq6iv8VNz25QA6iByEjsu2r75PPoGjalqmfr-HMLb0uT6vvYdLYHbeIJgJrUsQGHkXE66kOC3XRD6BFG9KOcDupEaYm-nmP22hhZqIODXkSMR85ZrRUxN3aF3JBz6jL3Z_Y2igfzcoc8JBhhrK61_R7-lwrkBwx8O1OxlkkGYfRY77ysRfzNGZUq9Eyc-7Kc0SMsv6BDpHANvJEpYJE1wWZcsoVxiJaoOIqBrT1Lze_nmQZHpm_vf1LyKqZV6pJmSNsMfXYnA4wrlRvt02Tujj5Q42J_M4PXywaRf35rCpO1s0VuFsfRs3gNiJurhjvUjB85ze9-Wcf4cPKXEFfLS0Dz7DVKKhXUAHn0H82FOquER8XX3aIcprl3nvEaPqhzaU7bX1Pq5p7kP5Se5S9WqSsMp7Ey50XKTooTi54P4uVFDSqUP19do_LvxmfmiULzBa8If8yzRt9pe0MdscOepgGMDDjhh-DuSibEKO9WQ_7tdmskJZv7PaM-ww2c3ofWt3QIQhLOjmxNIYvcokc6Cx0kJr7resFWxSbX0fYWF0mQ=w64-h64-c",
"thumbnail": "https://lh3.googleusercontent.com/lr/AAJ1LKeQlMlh8_iaFrzN1yCLbafP9AkStDG83hCOdaYYht7MwwXqkWrUrNgcJ2AHfMPNo0zfdqg9zdhaLS1CTJhor0dXES7yE6U1wahFRZFt96GlX_mD2x_HpF66t0ujXgO_6-wGjbUk0iX-ckNDxV84oXu_2Cot_ywEYORxk9VF5NFavYevvlaBm6gnZnNyaT0vnicA4KcaYk_Od46IIrRMnG6Ubh2ChcEn0rOjLwquw33obr9QVDuV3vc2iHCajO9vkRLEvQEMatbO_713nSPTPm7HLTd-D3dPHBiVMZUefkJD1lH_WPwtL83N95krnrBpwg-xP6_9R2EmDgRka4rLOU-Om7cvNnrgjDzri1PQSJIqO8dAjUnMr83XfS8yLYgrESIk0v_gbgQww7GqkGBra98tgVrF-vgtH_4Hiv5QW89QcsOLSsYpOuuXikFq6iv8VNz25QA6iByEjsu2r75PPoGjalqmfr-HMLb0uT6vvYdLYHbeIJgJrUsQGHkXE66kOC3XRD6BFG9KOcDupEaYm-nmP22hhZqIODXkSMR85ZrRUxN3aF3JBz6jL3Z_Y2igfzcoc8JBhhrK61_R7-lwrkBwx8O1OxlkkGYfRY77ysRfzNGZUq9Eyc-7Kc0SMsv6BDpHANvJEpYJE1wWZcsoVxiJaoOIqBrT1Lze_nmQZHpm_vf1LyKqZV6pJmSNsMfXYnA4wrlRvt02Tujj5Q42J_M4PXywaRf35rCpO1s0VuFsfRs3gNiJurhjvUjB85ze9-Wcf4cPKXEFfLS0Dz7DVKKhXUAHn0H82FOquER8XX3aIcprl3nvEaPqhzaU7bX1Pq5p7kP5Se5S9WqSsMp7Ey50XKTooTi54P4uVFDSqUP19do_LvxmfmiULzBa8If8yzRt9pe0MdscOepgGMDDjhh-DuSibEKO9WQ_7tdmskJZv7PaM-ww2c3ofWt3QIQhLOjmxNIYvcokc6Cx0kJr7resFWxSbX0fYWF0mQ=w300-h300-c",
"id": "ABlQyLyns2Vr5CcPO1btRTXxf911QauEdSzuBLOZ7HYCJUpVFF6cQojQWSqg6BQLrNqEqKdLvweA",
"requestPath": "ABlQyLyns2Vr5CcPO1btRTXxf911QauEdSzuBLOZ7HYCJUpVFF6cQojQWSqg6BQLrNqEqKdLvweA"
},
{
"isFolder": true,
"icon": "https://lh3.googleusercontent.com/lr/AAJ1LKcmaO0eq_PNl-Qh_dIReRLFKXMJlXjU2C86rrkAvqu76xFlDEV10-0qIqZRou1FLl6LouznwXnmeYOELDSAc_8bYHnJpPY6ZBcS_Z-h2O4iaSGmIRxlaTrHhuYwuqlN5dJeEPzktUq0-OD10IA8RdKqKnCYYSlOCDbKOzgSimDTUhyepZAHUZ2UHVNLAed7JPyJSTkSv_ldMSDmjSdP1Rm7aEHGDSAvLHLSUAKvjBZTTP0a6UN5xDQfZVd0jJSXReXQRh5WZZgNNkOw_Ur_6OTiixqqHmUnBbC89yLKkw4shLaNpVC8lOCzORLFzvmwdESD0Kl_XWL3Q-H3sdqmtZw42oEe7XRi1n4q70HOqBV2rhg-32XRZU2ZZYxyKP9hVG95Afw8JJD8Ap32K9C14sfL87nob24VYmaGAnfH8erIb703IL9Sad2yZtvZFm-T1oopqwtw4aXjkEVWChsSZ0UW2qytav4zveKpSZSdethStqJZMq57_u97El6Jpk3hY9pPkQ5qgtzEKDQJK3i3DPl-QuvMDo1UZNacC67lOHFBWDSmKHWoLtvfZpdRQlaAnvoPPWTYnrLLVsWzkYEjLQxNHSf_cZFtS5vZ8sDLmQ4EdikrunTvGYmN_-E2ZgiLQhguuMhN38iGa9UBYNvHc2qWJAel6EDUgNqAZuKHYKDEnYPFYlZ0UxJn4eiM6bLhRUGzcIcnKrM2XKg0ZkvwWnXs5oWZ7QtPmIBHOkYvlmB7piZkgJL_nm6d9U2vf_KsGMi8R9FmlV8zqecwuZeo4MsI8hC6QcR1R1Kp-ZI99nUQFxx3bERf6kEFJdelK3qpFXOeI_B1cnifIUEtSRcP3P_VoR_VglmYGHt_k_w-xWTUjYruB9bWMuyuv4TgQ2eMBnN7GmpSsWRsbaYHmy-uuiThRXEmHAcPuenp48pbCf9F00i3qs55KDdqz9utFEKFHQkrGIHHwvZ5TDypIPLw1HVNQQn1tqFiZJAvEgY=w64-h64-c",
"thumbnail": "https://lh3.googleusercontent.com/lr/AAJ1LKcmaO0eq_PNl-Qh_dIReRLFKXMJlXjU2C86rrkAvqu76xFlDEV10-0qIqZRou1FLl6LouznwXnmeYOELDSAc_8bYHnJpPY6ZBcS_Z-h2O4iaSGmIRxlaTrHhuYwuqlN5dJeEPzktUq0-OD10IA8RdKqKnCYYSlOCDbKOzgSimDTUhyepZAHUZ2UHVNLAed7JPyJSTkSv_ldMSDmjSdP1Rm7aEHGDSAvLHLSUAKvjBZTTP0a6UN5xDQfZVd0jJSXReXQRh5WZZgNNkOw_Ur_6OTiixqqHmUnBbC89yLKkw4shLaNpVC8lOCzORLFzvmwdESD0Kl_XWL3Q-H3sdqmtZw42oEe7XRi1n4q70HOqBV2rhg-32XRZU2ZZYxyKP9hVG95Afw8JJD8Ap32K9C14sfL87nob24VYmaGAnfH8erIb703IL9Sad2yZtvZFm-T1oopqwtw4aXjkEVWChsSZ0UW2qytav4zveKpSZSdethStqJZMq57_u97El6Jpk3hY9pPkQ5qgtzEKDQJK3i3DPl-QuvMDo1UZNacC67lOHFBWDSmKHWoLtvfZpdRQlaAnvoPPWTYnrLLVsWzkYEjLQxNHSf_cZFtS5vZ8sDLmQ4EdikrunTvGYmN_-E2ZgiLQhguuMhN38iGa9UBYNvHc2qWJAel6EDUgNqAZuKHYKDEnYPFYlZ0UxJn4eiM6bLhRUGzcIcnKrM2XKg0ZkvwWnXs5oWZ7QtPmIBHOkYvlmB7piZkgJL_nm6d9U2vf_KsGMi8R9FmlV8zqecwuZeo4MsI8hC6QcR1R1Kp-ZI99nUQFxx3bERf6kEFJdelK3qpFXOeI_B1cnifIUEtSRcP3P_VoR_VglmYGHt_k_w-xWTUjYruB9bWMuyuv4TgQ2eMBnN7GmpSsWRsbaYHmy-uuiThRXEmHAcPuenp48pbCf9F00i3qs55KDdqz9utFEKFHQkrGIHHwvZ5TDypIPLw1HVNQQn1tqFiZJAvEgY=w300-h300-c",
"id": "ABlQyLy4ph0jPO0yC50yRPxaYHaFdzMR82O5y5SkbFdUeb1UPtp266rcibJsI8PZ1xmR_0iWrbCo",
"requestPath": "ABlQyLy4ph0jPO0yC50yRPxaYHaFdzMR82O5y5SkbFdUeb1UPtp266rcibJsI8PZ1xmR_0iWrbCo"
},
{
"isFolder": true,
"icon": "https://lh3.googleusercontent.com/lr/AAJ1LKeRK4usIAiFOw-Ji2B41UYN8Jw5_QZoXOSRE1LA3MZBiUBuMIrbggQXe_dLJ8dIPxPtZrvXNu3l8fyrl-qLV-65SSrUJlI_6HvFuWtnHaBBTQgX4tZ_rRbM8DLKIrqvjkjstW8okeXE6CehBMg8RxZ7Irvu4DjYvrYReatW6hWNnu1UirPdn8Oaxd8df8pHwjkv0CFpn9Codm0RLlt5qYr6TjJ4rl9e1kATD5Rw5EVLEyIEaai1zQhfLptQXyyWIUf-iK4rSUWm_X4FMx9BroeP_JwVtuMS21F_cegfD7suvawvLWgpCCcBI7sXyO02M9Qu7VYpnNqYpcffa3coirLbqIVxxjwIPmJ4WK25ivvgaehKTAZEtXK4AOZ65LhRX9I3-5ZgHuIdx-4iiF_ootFtdyZaVk6Zhbi1tSRs1lEv7zhdDF1tFqM9VS_glCTdOM9cERGUQkmEULvRyantppAi62q5FxIwATIvL2LWHwNbDIMfZl6W8SYKMxAzZ7Pk6Vz7Vyjy8yRMidO650c64QG1eKeTnalCctHIEd33ihDBXzKAteUtUESke6oP8KDMZ5vgD4b5O5rmVpnv6dO6X5VGDNDPD2gv6WjKJMfjy1Ns-5AtZmSPDL7U8r8aQgoCUQXuc8ohPnlkDfR9026nGuqs056mhARd9vrcV3fspyZamKmQ4BtdWCAhYsHgkd7DonmMCkBBQl9zSMDhjQ42votyi6sVHOXmik5LtibCy-ilTT26qg4FuWOfyCJtfN27RGQ00YRmhtWBc6CCSQdYk7ycAxBmC0MvC_3jX8Wbma2FAtNKVDGBiHw0PFEKqlHtyQTVXwbQZKs6rvYQwrEFO2NwixB68hpCBp3aETyDwE_xCMCl7Z7gj6z0z67TgaikxQ_jZUPKXKygYsHxihFQuwt474lmmcZkYq_TPb0zVDo6G5T-F5orDcRsAvH1rybazzIYvbii9GFlwIiaIK3LfWkpttEvFFUmSY52qQ=w64-h64-c",
"thumbnail": "https://lh3.googleusercontent.com/lr/AAJ1LKeRK4usIAiFOw-Ji2B41UYN8Jw5_QZoXOSRE1LA3MZBiUBuMIrbggQXe_dLJ8dIPxPtZrvXNu3l8fyrl-qLV-65SSrUJlI_6HvFuWtnHaBBTQgX4tZ_rRbM8DLKIrqvjkjstW8okeXE6CehBMg8RxZ7Irvu4DjYvrYReatW6hWNnu1UirPdn8Oaxd8df8pHwjkv0CFpn9Codm0RLlt5qYr6TjJ4rl9e1kATD5Rw5EVLEyIEaai1zQhfLptQXyyWIUf-iK4rSUWm_X4FMx9BroeP_JwVtuMS21F_cegfD7suvawvLWgpCCcBI7sXyO02M9Qu7VYpnNqYpcffa3coirLbqIVxxjwIPmJ4WK25ivvgaehKTAZEtXK4AOZ65LhRX9I3-5ZgHuIdx-4iiF_ootFtdyZaVk6Zhbi1tSRs1lEv7zhdDF1tFqM9VS_glCTdOM9cERGUQkmEULvRyantppAi62q5FxIwATIvL2LWHwNbDIMfZl6W8SYKMxAzZ7Pk6Vz7Vyjy8yRMidO650c64QG1eKeTnalCctHIEd33ihDBXzKAteUtUESke6oP8KDMZ5vgD4b5O5rmVpnv6dO6X5VGDNDPD2gv6WjKJMfjy1Ns-5AtZmSPDL7U8r8aQgoCUQXuc8ohPnlkDfR9026nGuqs056mhARd9vrcV3fspyZamKmQ4BtdWCAhYsHgkd7DonmMCkBBQl9zSMDhjQ42votyi6sVHOXmik5LtibCy-ilTT26qg4FuWOfyCJtfN27RGQ00YRmhtWBc6CCSQdYk7ycAxBmC0MvC_3jX8Wbma2FAtNKVDGBiHw0PFEKqlHtyQTVXwbQZKs6rvYQwrEFO2NwixB68hpCBp3aETyDwE_xCMCl7Z7gj6z0z67TgaikxQ_jZUPKXKygYsHxihFQuwt474lmmcZkYq_TPb0zVDo6G5T-F5orDcRsAvH1rybazzIYvbii9GFlwIiaIK3LfWkpttEvFFUmSY52qQ=w300-h300-c",
"id": "ABlQyLxqVv8em1iki29QEpG5yE8V0ucryQgnFHGoArHHTwv4GOxtjZly7cyfcx5curEYSoNoV6BH",
"requestPath": "ABlQyLxqVv8em1iki29QEpG5yE8V0ucryQgnFHGoArHHTwv4GOxtjZly7cyfcx5curEYSoNoV6BH"
}
],
"nextPagePath": null
}
Yes I think that would be better. Maybe check with @nqst if you need an SVG. |
What is interesting is that I only have one album in Google Photos, which is the one with a name. I have no idea where the others came from. We could also not render albums without a name? |
I agree it's better to show a folder icon in this view mode (when the icons are small and we don't show the amount of items in albums). How about reusing the same icon that we use in Google Drive for consistency? We currently use this one but I suggest using the 32x32 version (and scaling it down via |
have now merged main and updated the folder icon. as for the missing folder name, let's just leave it as is because we don't know how to reproduce it? |
closes #2163
remaining tasks:
Partner program
Potential problems
links: