Merge remote-tracking branch 'origin/album_grid_UI' into widget-superpowered

This commit is contained in:
Prateek Sunal
2025-05-14 11:21:50 +05:30
10 changed files with 257 additions and 18 deletions

View 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

View 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,
),
],
),
),
);
}
}

View File

@@ -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,
),
],
),

View File

@@ -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),
),

View File

@@ -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

View File

@@ -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 {

View File

@@ -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,
})

View File

@@ -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,
})

View File

@@ -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 {

View File

@@ -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(