Merge remote-tracking branch 'origin/album_grid_UI' into widget-superpowered
This commit is contained in:
77
.github/workflows/mobile-internal-release-rust.yml
vendored
Normal file
77
.github/workflows/mobile-internal-release-rust.yml
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
name: "Internal release (photos)"
|
||||
|
||||
on:
|
||||
workflow_dispatch: # Allow manually running the action
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.24.3"
|
||||
RUST_VERSION: "1.85.1"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: mobile
|
||||
|
||||
steps:
|
||||
- name: Checkout code and submodules
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup JDK 17
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 17
|
||||
|
||||
- name: Install Flutter ${{ env.FLUTTER_VERSION }}
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Install Rust ${{ env.RUST_VERSION }}
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ env.RUST_VERSION }}
|
||||
|
||||
- name: Install Flutter Rust Bridge
|
||||
run: cargo install flutter_rust_bridge_codegen
|
||||
|
||||
- name: Setup keys
|
||||
uses: timheuer/base64-to-file@v1
|
||||
with:
|
||||
fileName: "keystore/ente_photos_key.jks"
|
||||
encodedString: ${{ secrets.SIGNING_KEY_PHOTOS }}
|
||||
|
||||
- name: Build PlayStore AAB
|
||||
run: |
|
||||
flutter build appbundle --dart-define=cronetHttpNoPlay=true --release --flavor playstore
|
||||
env:
|
||||
SIGNING_KEY_PATH: "/home/runner/work/_temp/keystore/ente_photos_key.jks"
|
||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS_PHOTOS }}
|
||||
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD_PHOTOS }}
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD_PHOTOS }}
|
||||
|
||||
- name: Upload AAB to PlayStore
|
||||
uses: r0adkll/upload-google-play@v1
|
||||
with:
|
||||
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
||||
packageName: io.ente.photos
|
||||
releaseFiles: mobile/build/app/outputs/bundle/playstoreRelease/app-playstore-release.aab
|
||||
track: internal
|
||||
|
||||
- name: Notify Discord
|
||||
uses: sarisia/actions-status-discord@v1
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_INTERNAL_RELEASE_WEBHOOK }}
|
||||
nodetail: true
|
||||
title: "🏆 Internal release available for Photos"
|
||||
description: "[Download](https://play.google.com/store/apps/details?id=io.ente.photos)"
|
||||
color: 0x00ff00
|
||||
96
mobile/lib/ui/collections/album/new_row_item.dart
Normal file
96
mobile/lib/ui/collections/album/new_row_item.dart
Normal file
@@ -0,0 +1,96 @@
|
||||
import "package:dotted_border/dotted_border.dart";
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:logging/logging.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import 'package:photos/models/collection/collection.dart';
|
||||
import 'package:photos/models/collection/collection_items.dart';
|
||||
import "package:photos/services/collections_service.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import "package:photos/ui/viewer/gallery/collection_page.dart";
|
||||
import "package:photos/utils/dialog_util.dart";
|
||||
import "package:photos/utils/navigation_util.dart";
|
||||
|
||||
class NewAlbumRowItemWidget extends StatelessWidget {
|
||||
final Color? color;
|
||||
final double height;
|
||||
final double width;
|
||||
const NewAlbumRowItemWidget({
|
||||
this.color,
|
||||
super.key,
|
||||
required this.height,
|
||||
required this.width,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final enteColorScheme = getEnteColorScheme(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2.5),
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
final result = await showTextInputDialog(
|
||||
context,
|
||||
title: S.of(context).newAlbum,
|
||||
submitButtonLabel: S.of(context).create,
|
||||
hintText: S.of(context).enterAlbumName,
|
||||
alwaysShowSuccessState: false,
|
||||
initialValue: "",
|
||||
textCapitalization: TextCapitalization.words,
|
||||
popnavAfterSubmission: false,
|
||||
onSubmit: (String text) async {
|
||||
if (text.trim() == "") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final Collection c =
|
||||
await CollectionsService.instance.createAlbum(text);
|
||||
// ignore: unawaited_futures
|
||||
await routeToPage(
|
||||
context,
|
||||
CollectionPage(CollectionWithThumbnail(c, null)),
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
} catch (e, s) {
|
||||
Logger("CreateNewAlbumRowItemWidget")
|
||||
.severe("Failed to rename album", e, s);
|
||||
rethrow;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (result is Exception) {
|
||||
await showGenericErrorDialog(context: context, error: result);
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
DottedBorder(
|
||||
borderType: BorderType.RRect,
|
||||
strokeWidth: 1.5,
|
||||
borderPadding: const EdgeInsets.all(0.75),
|
||||
dashPattern: const [3.75, 3.75],
|
||||
radius: const Radius.circular(2.35),
|
||||
padding: EdgeInsets.zero,
|
||||
color: enteColorScheme.blurStrokePressed,
|
||||
child: SizedBox(
|
||||
height: height,
|
||||
width: width,
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
color: enteColorScheme.blurStrokePressed,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
S.of(context).addNew,
|
||||
style: getEnteTextTheme(context).smallFaint,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,12 @@ class _CollectionListPageState extends State<CollectionListPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final displayLimitCount = (collections?.length ?? 0);
|
||||
final bool enableSelectionMode =
|
||||
widget.sectionType == UISectionType.homeCollections ||
|
||||
widget.sectionType == UISectionType.outgoingCollections ||
|
||||
widget.sectionType == UISectionType.incomingCollections;
|
||||
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: CustomScrollView(
|
||||
@@ -75,8 +81,10 @@ class _CollectionListPageState extends State<CollectionListPage> {
|
||||
),
|
||||
CollectionsFlexiGridViewWidget(
|
||||
collections,
|
||||
displayLimitCount: collections?.length ?? 0,
|
||||
displayLimitCount: displayLimitCount,
|
||||
tag: widget.tag,
|
||||
enableSelectionMode: enableSelectionMode,
|
||||
scrollBottomSafeArea: 140,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -2,16 +2,17 @@ import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:photos/models/collection/collection.dart';
|
||||
import "package:photos/ui/collections/album/new_row_item.dart";
|
||||
import "package:photos/ui/collections/album/row_item.dart";
|
||||
|
||||
class CollectionsFlexiGridViewWidget extends StatelessWidget {
|
||||
class CollectionsFlexiGridViewWidget extends StatefulWidget {
|
||||
/*
|
||||
Aspect ratio 1:1 Max width 224 Fixed gap 8
|
||||
Width changes dynamically with screen width such that we can fit 2 in one row.
|
||||
Keep the width integral (center the albums to distribute excess pixels)
|
||||
*/
|
||||
static const maxThumbnailWidth = 224.0;
|
||||
static const fixedGapBetweenAlbum = 8.0;
|
||||
static const maxThumbnailWidth = 170.0;
|
||||
static const fixedGapBetweenAlbum = 2.0;
|
||||
static const minGapForHorizontalPadding = 8.0;
|
||||
static const collectionItemsToPreload = 20;
|
||||
|
||||
@@ -23,45 +24,83 @@ class CollectionsFlexiGridViewWidget extends StatelessWidget {
|
||||
final bool shrinkWrap;
|
||||
final String tag;
|
||||
|
||||
final bool enableSelectionMode;
|
||||
final bool shouldShowCreateAlbum;
|
||||
final double scrollBottomSafeArea;
|
||||
|
||||
const CollectionsFlexiGridViewWidget(
|
||||
this.collections, {
|
||||
this.displayLimitCount = 10,
|
||||
this.shrinkWrap = false,
|
||||
this.tag = "",
|
||||
super.key,
|
||||
this.enableSelectionMode = false,
|
||||
this.shouldShowCreateAlbum = false,
|
||||
this.scrollBottomSafeArea = 8,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CollectionsFlexiGridViewWidget> createState() =>
|
||||
_CollectionsFlexiGridViewWidgetState();
|
||||
}
|
||||
|
||||
class _CollectionsFlexiGridViewWidgetState
|
||||
extends State<CollectionsFlexiGridViewWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double screenWidth = MediaQuery.of(context).size.width;
|
||||
final int albumsCountInOneRow = max(screenWidth ~/ maxThumbnailWidth, 2);
|
||||
final double gapBetweenAlbums =
|
||||
(albumsCountInOneRow - 1) * fixedGapBetweenAlbum;
|
||||
final int albumsCountInOneRow =
|
||||
max(screenWidth ~/ CollectionsFlexiGridViewWidget.maxThumbnailWidth, 3);
|
||||
final double gapBetweenAlbums = (albumsCountInOneRow - 1) *
|
||||
CollectionsFlexiGridViewWidget.fixedGapBetweenAlbum;
|
||||
// gapOnSizeOfAlbums will be
|
||||
final double gapOnSizeOfAlbums = minGapForHorizontalPadding +
|
||||
(screenWidth - gapBetweenAlbums - (2 * minGapForHorizontalPadding)) %
|
||||
albumsCountInOneRow;
|
||||
final double gapOnSizeOfAlbums =
|
||||
CollectionsFlexiGridViewWidget.minGapForHorizontalPadding +
|
||||
(screenWidth -
|
||||
gapBetweenAlbums -
|
||||
(2 *
|
||||
CollectionsFlexiGridViewWidget
|
||||
.minGapForHorizontalPadding)) %
|
||||
albumsCountInOneRow;
|
||||
|
||||
final double sideOfThumbnail =
|
||||
(screenWidth - gapOnSizeOfAlbums - gapBetweenAlbums) /
|
||||
albumsCountInOneRow;
|
||||
|
||||
final int totalCollections = widget.collections!.length;
|
||||
final bool showCreateAlbum = widget.shouldShowCreateAlbum;
|
||||
final int totalItemCount = totalCollections + (showCreateAlbum ? 1 : 0);
|
||||
final int displayItemCount = min(totalItemCount, widget.displayLimitCount);
|
||||
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
padding: EdgeInsets.only(
|
||||
top: 8,
|
||||
left: 8,
|
||||
right: 8,
|
||||
bottom: widget.scrollBottomSafeArea,
|
||||
),
|
||||
sliver: SliverGrid(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
if (showCreateAlbum && index == 0) {
|
||||
return NewAlbumRowItemWidget(
|
||||
height: sideOfThumbnail,
|
||||
width: sideOfThumbnail,
|
||||
);
|
||||
}
|
||||
final collectionIndex = showCreateAlbum ? index - 1 : index;
|
||||
return AlbumRowItemWidget(
|
||||
collections![index],
|
||||
widget.collections![collectionIndex],
|
||||
sideOfThumbnail,
|
||||
tag: tag,
|
||||
tag: widget.tag,
|
||||
showFileCount: false,
|
||||
);
|
||||
},
|
||||
childCount: min(collections!.length, displayLimitCount),
|
||||
childCount: displayItemCount,
|
||||
),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: albumsCountInOneRow,
|
||||
mainAxisSpacing: 4,
|
||||
mainAxisSpacing: 2,
|
||||
crossAxisSpacing: gapBetweenAlbums,
|
||||
childAspectRatio: sideOfThumbnail / (sideOfThumbnail + 46),
|
||||
),
|
||||
|
||||
@@ -57,7 +57,7 @@ class _UserCollectionsTabState extends State<UserCollectionsTab>
|
||||
leading: true,
|
||||
);
|
||||
|
||||
static const int _kOnEnteItemLimitCount = 10;
|
||||
static const int _kOnEnteItemLimitCount = 12;
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -164,6 +164,8 @@ class _UserCollectionsTabState extends State<UserCollectionsTab>
|
||||
collections,
|
||||
displayLimitCount: _kOnEnteItemLimitCount,
|
||||
shrinkWrap: true,
|
||||
shouldShowCreateAlbum: true,
|
||||
enableSelectionMode: true,
|
||||
)
|
||||
: const SliverToBoxAdapter(child: EmptyState()),
|
||||
collections.length > _kOnEnteItemLimitCount
|
||||
|
||||
@@ -63,8 +63,9 @@ func (g *GetFilesData) Validate() error {
|
||||
}
|
||||
|
||||
type GetFileData struct {
|
||||
FileID int64 `form:"fileID" binding:"required"`
|
||||
Type ente.ObjectType `form:"type" binding:"required"`
|
||||
FileID int64 `form:"fileID" binding:"required"`
|
||||
Type ente.ObjectType `form:"type" binding:"required"`
|
||||
PreferNoContent bool `form:"preferNoContent"`
|
||||
}
|
||||
|
||||
func (g *GetFileData) Validate() error {
|
||||
|
||||
@@ -109,6 +109,10 @@ func (h *FileHandler) GetFileData(ctx *gin.Context) {
|
||||
handler.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
if resp == nil {
|
||||
ctx.Status(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, gin.H{
|
||||
"data": resp,
|
||||
})
|
||||
|
||||
@@ -79,6 +79,10 @@ func (h *PublicCollectionHandler) GetFileData(c *gin.Context) {
|
||||
handler.Error(c, err)
|
||||
return
|
||||
}
|
||||
if resp == nil {
|
||||
c.Status(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"data": resp,
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ package filedata
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
@@ -138,6 +139,9 @@ func (c *Controller) GetFileData(ctx *gin.Context, actorUser int64, req fileData
|
||||
}
|
||||
doRows, err := c.Repo.GetFilesData(ctx, req.Type, []int64{req.FileID})
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) && req.PreferNoContent {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, stacktrace.Propagate(err, "")
|
||||
}
|
||||
if len(doRows) == 0 || doRows[0].IsDeleted {
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import { safeFileName } from "ente-new/photos/utils/native-fs";
|
||||
import { getData } from "ente-shared/storage/localStorage";
|
||||
import type { User } from "ente-shared/user/types";
|
||||
import { wait } from "ente-utils/promise";
|
||||
import { t } from "i18next";
|
||||
import {
|
||||
addMultipleToFavorites,
|
||||
@@ -60,6 +61,9 @@ export async function downloadFile(file: EnteFile) {
|
||||
new Blob([videoData], { type: videoType.mimeType }),
|
||||
);
|
||||
downloadAndRevokeObjectURL(tempImageURL, imageFileName);
|
||||
// Downloading multiple works everywhere except, you guessed it,
|
||||
// Safari. Make up for their incompetence by adding a setTimeout.
|
||||
await wait(300) /* arbitrary constant, 300ms */;
|
||||
downloadAndRevokeObjectURL(tempVideoURL, videoFileName);
|
||||
} else {
|
||||
const fileType = await detectFileTypeInfo(
|
||||
|
||||
Reference in New Issue
Block a user